Reusable workflows

13 minute read

Reusable workflows streamline shared automation across projects. They act as modular building blocks: one workflow runs another as a job. A reusable workflow must include a workflow_call trigger and at least one job.

Reusable workflow access to caller workflow vars or secrets is not automatic. Pass these values as inputs or enable inheritance with vars: inherit or secrets: inherit in the caller workflow.

A caller workflow can pass an environment name to a reusable workflow as an inputs value. Specifying that value in a reusable workflow job environment expression enables access to that environment. However, reusable workflow access environment-specific secrets and vars is allowed only when the caller job has inheritance enabled. Environment approvals and OIDC token issuance apply when the job targets that environment.

Refer to:

Considerations

  • 4 is the maximum number of reusable workflow levels, including the caller workflow e.g., A → B → C → D → E; E exceeds the limit, where A is the caller workflow and B, C, D, and E are the reusable workflows.

  • Twenty is the maximum number of reusable workflow (workflow_call) triggers allowed within the hierarchy of the caller workflow.

  • By default, reusable workflow jobs do not inherit vars or secrets from the caller workflow. Configure inheritance in the caller workflow by enabling inheritance with vars: inherit or secrets: inherit.

  • A reusable workflow job can access a caller workflow environment using an environment: inputs expression. e.g. environment: ${{ inputs.env-prod }}. However, the caller job must have secrets: inherit enabled to provide access to that environment’s secrets and vars.

  • Cross-organization restriction: If a reusable workflow references an environment in one of its jobs, the reusable workflow must reside within the same SCM organization as the caller workflow.

    This is a security feature to ensure environments defined within a CloudBees organization are only accessible to reusable workflows created in repositories belonging to the same SCM organization as the repository of the caller workflow. Reusable workflows residing in an SCM organization, different from the one of the caller workflow, must rely solely on vars and secrets passed as inputs explicitly by the caller; therefore caller is in full control of the vars and secrets visible to the reusable workflow.

  • Use the absolute path to the reusable workflow in the job’s uses field:

    • {owner}/{repo}/.cloudbees/workflows/{filename}

    • {owner}/{repo}/.cloudbees/workflows/{filename}@{ref} to pin to a branch, tag, or commit

Refer to:

Examples of DSL syntax allowing dynamic specification of environments and inheritance of vars and secrets

The examples in this section take a conservative stance on inheritance and environment parameterization (not environment mapping). These align with GitHub’s DSL syntax and behavior, and leave room for future expansion of inheritance and mapping based on user feedback.

Considerations:

  • Secrets inheritance (Required) — Implemented similar to GitHub.

  • Environments as inputs (Required) — Environments can be passed as inputs to the reusable workflow. Any job within the reusable workflow can access the environment passed from the caller workflow via the inputs expression.

  • Vars inheritance (Required) — GitHub passes all vars from the scope of the caller job to the reusable workflow by default (including environment vars). In our DSL, we restrict this to avoid exposing vars via OIDC subject claims to third‑party workflows or workflows from another organization. Automatic inheritance could pose a security risk if the reusable workflow job can assume a role associated with that environment and use environment variables and secrets to perform unauthorized operations against a deployment target.

  • Vars mapping (optional) — Not required in GitHub because it’s implicit. However, to mitigate the OIDC subject‑claim risk when environments are referenced, we need to expose mapping so reusable workflow jobs can access vars from the caller workflow’s scope.

When vars and secrets are passed as mapped values, delayed expression evaluation is required. If a job specifies an environment using an expression (for example, ${{ inputs.env-prod }} in the examples below), first resolve the environment name and then evaluate vars and secrets, since their values can come from that environment.

Example 1: Caller job specifies secrets inheritance and passes an environment as input to the reusable workflow

In this example:

  • The caller job uses secrets: inherit, so all secrets in the caller job’s scope (including any environment-scoped secrets for that job) are available to the reusable workflow.

  • The caller passes two inputs: a simple string (var-1) and an environment name (env-prod).

  • job-rw-1 can read all inherited secrets. It does not have access to any caller vars.

  • job-rw-2 runs in the provided environment (production), so environment secrets override inherited secrets if both are defined. vars are still not available.

View workflow example
Caller workflow
# File: .cloudbees/workflows/call-inner-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: caller-workflow on: push: (1) branches: [ main ] jobs: caller-job: uses: .cloudbees/workflows/a-reusable-workflow.yaml secrets: inherit (2) inputs: var-1: ${{ vars.var-A }} (3) env-prod: production (4)
1 Triggers the workflow on pushes to the main branch.
2 Inherits all secrets from the caller job’s scope for use by the reusable workflow.
3 Passes a string input from a caller var. (The reusable workflow cannot read caller vars directly; values must be passed via inputs.)
4 Passes the environment name to the reusable workflow so a job can resolve environment-scoped secrets.
Reusable workflow
# File: .cloudbees/workflows/a-reusable-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: reusable-workflow on: workflow_call: inputs: (1) var-1: type: string required: false env-prod: type: string required: true jobs: job-rw-1: steps: (2) - name: Use inherited secrets run: | # Inherited secrets are available; vars are not. if [ -n "${{ secrets.secret-1 }}" ]; then echo "secret-1 available"; fi if [ -n "${{ secrets.secret-2 }}" ]; then echo "secret-2 available"; fi # 'var-1' is only available via inputs, not as a var: if [ -n "${{ inputs.var-1 }}" ]; then echo "var-1 provided as input"; fi job-rw-2: environment: ${{ inputs.env-prod }} (3) steps: (4) - name: Use environment-resolved secrets (override if present) run: | # If the environment defines secret-1/secret-2, those values override inherited ones. if [ -n "${{ secrets.secret-1 }}" ]; then echo "secret-1 available (env may override)"; fi if [ -n "${{ secrets.secret-2 }}" ]; then echo "secret-2 available (env may override)"; fi # 'vars' are not accessible; only inputs and secrets are available.
1 Declares inputs accepted from the caller workflow.
2 In job-rw-1, all inherited secrets are available; caller vars are not. Use inputs for any values from the caller.
3 Sets the job’s environment from env-prod, enabling environment-scoped secret resolution.
4 In job-rw-2, if the environment defines secret-1 and secret-2, those override inherited values. vars are not available from either the caller or the environment.

Example 2: Caller job specifies secrets mapping and passes an environment as input to the reusable workflow

In this example:

  • The caller job maps only two secrets for the reusable workflow: secret-1 and secret-2. No other secrets are available to the reusable workflow.

  • The caller passes two inputs: a simple string (var-1) and an environment name (env-prod).

  • job-rw-1 can read secret-1 and secret-2. It does not have access to caller vars; use inputs for values.

  • job-rw-2 runs in the provided environment (production). If that environment defines secret-A and secret-B, those values override the caller mappings; otherwise, the mapped caller secrets are used. vars are not available.

View workflow example
Caller workflow
# File: .cloudbees/workflows/call-inner-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: caller-workflow on: push: (1) branches: [ main ] jobs: caller-job: uses: .cloudbees/workflows/a-reusable-workflow.yaml secrets: secret-1: ${{ secrets.secret-A }} (2) secret-2: ${{ secrets.secret-B }} (3) inputs: var-1: ${{ vars.var-A }} (4) env-prod: production (5)
1 Triggers the workflow on pushes to the main branch.
2 Maps secret-1 in the reusable workflow to the caller job’s secret secret-A.
3 Maps secret-2 in the reusable workflow to the caller job’s secret secret-B.
4 Passes a string value from a caller var via inputs (reusable workflows cannot read caller vars directly).
5 Passes the environment name used by the reusable workflow to resolve environment-scoped secrets.
Reusable workflow
# File: .cloudbees/workflows/a-reusable-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: reusable-workflow on: workflow_call: inputs: (1) var-1: type: string required: false env-prod: type: string required: true jobs: job-rw-1: steps: (2) - name: Use mapped secrets and input run: | if [ -n "${{ secrets.secret-1 }}" ]; then echo "secret-1 available"; fi if [ -n "${{ secrets.secret-2 }}" ]; then echo "secret-2 available"; fi if [ -n "${{ inputs.var-1 }}" ]; then echo "var-1 provided as input"; fi job-rw-2: environment: ${{ inputs.env-prod }} (3) steps: (4) - name: Use environment-resolved secrets (override if present) run: | # If the environment defines secret-A/secret-B, those values override the caller mappings. if [ -n "${{ secrets.secret-1 }}" ]; then echo "secret-1 available"; fi if [ -n "${{ secrets.secret-2 }}" ]; then echo "secret-2 available"; fi # 'vars' are not accessible to the reusable workflow; use inputs instead.
1 Declares the inputs accepted from the caller.
2 In job-rw-1, only the mapped secrets (secret-1, secret-2) are available; caller vars are not. var-1 is available via inputs.
3 Sets the job’s environment from env-prod, enabling environment-scoped secret resolution.
4 In job-rw-2, if the environment defines secret-A and secret-B, those values override the caller job mappings; otherwise, the mapped caller secrets are used. vars are not available from either the caller or the environment.

Example 3: Caller workflow job inherits all vars and passes an environment name

In this example:

  • The caller job uses vars: inherit, so all vars in the caller job’s scope (including any environment-scoped vars for that job) are available to the reusable workflow.

  • The caller passes two inputs: a simple string (var-1) and an environment name (env-prod).

  • job-rw-1 can read all inherited vars. It does not have access to any caller secrets.

  • job-rw-2 runs in the provided environment (production). If that environment defines vars with the same names, those values override inherited ones. Secrets are still not available.

View workflow example
Caller workflow
# File: .cloudbees/workflows/call-inner-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: caller-workflow on: push: (1) branches: [ main ] jobs: caller-job: uses: .cloudbees/workflows/a-reusable-workflow.yaml vars: inherit (2) inputs: var-1: ${{ vars.var-A }} (3) env-prod: production (4)
1 Triggers the workflow on pushes to the main branch.
2 Inherits all vars from the caller job’s scope for use by the reusable workflow.
3 Passes a string value from a caller var via inputs (reusable workflows cannot read caller vars passed implicitly unless inherited; inputs is explicit).
4 Passes the environment name used by the reusable workflow to resolve environment-scoped vars.
Reusable workflow
# File: .cloudbees/workflows/a-reusable-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: reusable-workflow on: workflow_call: inputs: (1) var-1: type: string required: false env-prod: type: string required: true jobs: job-rw-1: steps: (2) - name: Use inherited vars and input run: | # Inherited vars are available; secrets are not. if [ -n "${{ vars.var-A }}" ]; then echo "var-A available"; fi if [ -n "${{ vars.var-B }}" ]; then echo "var-B available"; fi # 'var-1' is also available via inputs: if [ -n "${{ inputs.var-1 }}" ]; then echo "var-1 provided as input"; fi job-rw-2: environment: ${{ inputs.env-prod }} (3) steps: (4) - name: Use environment-resolved vars (override if present) run: | # If the environment defines var-A/var-B, those values override inherited ones. if [ -n "${{ vars.var-A }}" ]; then echo "var-A available (env may override)"; fi if [ -n "${{ vars.var-B }}" ]; then echo "var-B available (env may override)"; fi # Secrets are not accessible in this example.
1 Declares the inputs accepted from the caller.
2 In job-rw-1, all inherited vars are available; caller secrets are not.
3 Sets the job’s environment from env-prod, enabling environment-scoped var resolution.
4 In job-rw-2, if the environment defines var-A and var-B, those values override inherited ones. Secrets are not available from either the caller or the environment.

Example 4: Caller workflow job maps specific vars and passes an environment name

In this example:

  • The caller job maps only two vars for the reusable workflow: var-1 and var-2. No other vars are available to the reusable workflow.

  • The caller also passes two inputs: a simple string (var-1) and an environment name (env-prod).

  • job-rw-1 can read the mapped vars (var-1, var-2). It does not have access to caller secrets.

  • job-rw-2 runs in the provided environment (production). If that environment defines var-A and var-B, those values override the mapped caller vars; otherwise, the mapped caller vars are used. Secrets are still not available.

View workflow example
Caller workflow
# File: .cloudbees/workflows/call-inner-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: caller-workflow on: push: (1) branches: [ main ] jobs: caller-job: uses: .cloudbees/workflows/a-reusable-workflow.yaml vars: var-1: ${{ vars.var-A }} (2) var-2: ${{ vars.var-B }} (2) inputs: var-1: ${{ vars.var-A }} (3) env-prod: production (4)
1 Triggers the workflow on pushes to the main branch.
2 Maps the reusable workflow vars var-1 and var-2 from the caller job’s vars var-A and var-B.
3 Passes a string value via inputs so the reusable workflow can also read var-1 as an input.
4 Passes the environment name used by the reusable workflow to resolve environment-scoped vars.
Reusable workflow
# File: .cloudbees/workflows/a-reusable-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: reusable-workflow on: workflow_call: inputs: (1) var-1: type: string required: false env-prod: type: string required: true jobs: job-rw-1: steps: (2) - name: Use mapped vars and input run: | # Mapped vars are available; secrets are not. if [ -n "${{ vars.var-1 }}" ]; then echo "var-1 available (mapped)"; fi if [ -n "${{ vars.var-2 }}" ]; then echo "var-2 available (mapped)"; fi # 'var-1' is also available via inputs: if [ -n "${{ inputs.var-1 }}" ]; then echo "var-1 provided as input"; fi job-rw-2: environment: ${{ inputs.env-prod }} (3) steps: (4) - name: Use environment-resolved vars (override if present) run: | # If the environment defines var-A/var-B, those values override the mapped ones. if [ -n "${{ vars.var-1 }}" ]; then echo "var-1 available (env may override)"; fi if [ -n "${{ vars.var-2 }}" ]; then echo "var-2 available (env may override)"; fi # Secrets are not accessible in this example.
1 Declares the inputs accepted from the caller.
2 In job-rw-1, only the mapped vars (var-1, var-2) are available; caller secrets are not. var-1 is also accessible via inputs.
3 Sets the job’s environment from env-prod, enabling environment-scoped var resolution.
4 In job-rw-2, if the environment defines var-A and var-B, those values override the mapped caller vars; otherwise the mapped values are used. Secrets are not available from either the caller or the environment.

Example 5: Caller workflow job inherits all vars and secrets and passes an environment name

In this example:

  • The caller job uses vars: inherit and secrets: inherit, so all vars and secrets in the caller job’s scope (including any environment-scoped values for that job) are available to the reusable workflow.

  • The caller passes two inputs: a simple string (var-1) and an environment name (env-prod).

  • job-rw-1 can read all inherited vars and secrets.

  • job-rw-2 runs in the provided environment (production). If that environment defines vars or secrets with the same names, those values override inherited ones.

View workflow example
Caller workflow
# File: .cloudbees/workflows/call-inner-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: caller-workflow on: push: (1) branches: [ main ] jobs: caller-job: uses: .cloudbees/workflows/a-reusable-workflow.yaml vars: inherit (2) secrets: inherit (3) inputs: var-1: ${{ vars.var-A }} (4) env-prod: production (5)
1 Triggers the workflow on pushes to the main branch.
2 Inherits all vars from the caller job’s scope for use by the reusable workflow.
3 Inherits all secrets from the caller job’s scope for use by the reusable workflow.
4 Passes a string value as an explicit input. (Even with vars: inherit, use inputs when you want to pass a specific value explicitly.)
5 Passes the environment name used by the reusable workflow to resolve environment-scoped vars and secrets.
Reusable workflow
# File: .cloudbees/workflows/a-reusable-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: reusable-workflow on: workflow_call: inputs: (1) var-1: type: string required: false env-prod: type: string required: true jobs: job-rw-1: steps: (2) - name: Use inherited vars and secrets (plus input) run: | # Inherited vars are available: if [ -n "${{ vars.var-A }}" ]; then echo "var-A available (inherited)"; fi if [ -n "${{ vars.var-B }}" ]; then echo "var-B available (inherited)"; fi # Inherited secrets are available: if [ -n "${{ secrets.secret-1 }}" ]; then echo "secret-1 available (inherited)"; fi if [ -n "${{ secrets.secret-2 }}" ]; then echo "secret-2 available (inherited)"; fi # 'var-1' is also available via inputs: if [ -n "${{ inputs.var-1 }}" ]; then echo "var-1 provided as input"; fi job-rw-2: environment: ${{ inputs.env-prod }} (3) steps: (4) - name: Use environment-resolved vars and secrets (override if present) run: | # If the environment defines var-A/var-B, those values override inherited ones: if [ -n "${{ vars.var-A }}" ]; then echo "var-A available (env may override)"; fi if [ -n "${{ vars.var-B }}" ]; then echo "var-B available (env may override)"; fi # If the environment defines secret-1/secret-2, those values override inherited ones: if [ -n "${{ secrets.secret-1 }}" ]; then echo "secret-1 available (env may override)"; fi if [ -n "${{ secrets.secret-2 }}" ]; then echo "secret-2 available (env may override)"; fi
1 Declares the inputs accepted from the caller.
2 In job-rw-1, all inherited vars and secrets are available.
3 Sets the job’s environment from env-prod, enabling environment-scoped resolution for vars and secrets.
4 In job-rw-2, if the environment defines values with the same names, those override inherited values.

Example 6: Caller workflow job maps specific vars and secrets and passes an environment name

In this example:

  • The caller job maps only two vars to the reusable workflow: var-1 and var-2. No other vars are available to the reusable workflow.

  • The caller job also maps only two secrets: secret-1 and secret-2. No other secrets are available to the reusable workflow.

  • The caller passes two inputs: a simple string (var-1) and an environment name (env-prod).

  • job-rw-1 can read the mapped vars and secrets. It does not have access to any unmapped values.

  • job-rw-2 runs in the provided environment (production). If that environment defines var-A/var-B or secret-A/secret-B, those values override the mapped ones. Unmapped values remain unavailable.

View workflow example
Caller workflow
# File: .cloudbees/workflows/call-inner-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: caller-workflow on: push: (1) branches: [ main ] jobs: caller-job: uses: .cloudbees/workflows/a-reusable-workflow.yaml vars: var-1: ${{ vars.var-A }} (2) var-2: ${{ vars.var-B }} (2) secrets: secret-1: ${{ secrets.secret-A }} (3) secret-2: ${{ secrets.secret-B }} (3) inputs: var-1: ${{ vars.var-A }} (4) env-prod: production (5)
1 Triggers the workflow on pushes to the main branch.
2 Maps the reusable workflow vars var-1 and var-2 from the caller job’s vars var-A and var-B.
3 Maps the reusable workflow secrets secret-1 and secret-2 from the caller job’s secrets secret-A and secret-B.
4 Passes a string value via inputs so the reusable workflow can also read var-1 as an input (optional, for illustration).
5 Passes the environment name used by the reusable workflow to resolve environment-scoped vars and secrets.
Reusable workflow
# File: .cloudbees/workflows/a-reusable-workflow.yaml apiVersion: automation.cloudbees.io/v1alpha1 kind: workflow name: reusable-workflow on: workflow_call: inputs: (1) var-1: type: string required: false env-prod: type: string required: true jobs: job-rw-1: steps: (2) - name: Use mapped vars and secrets (plus input) run: | # MAPPED VARS (from caller var-A/var-B): if [ -n "${{ vars.var-1 }}" ]; then echo "var-1 available (mapped)"; fi if [ -n "${{ vars.var-2 }}" ]; then echo "var-2 available (mapped)"; fi # MAPPED SECRETS (from caller secret-A/secret-B): if [ -n "${{ secrets.secret-1 }}" ]; then echo "secret-1 available (mapped)"; fi if [ -n "${{ secrets.secret-2 }}" ]; then echo "secret-2 available (mapped)"; fi # INPUT (explicitly passed): if [ -n "${{ inputs.var-1 }}" ]; then echo "var-1 provided as input"; fi job-rw-2: environment: ${{ inputs.env-prod }} (3) steps: (4) - name: Use environment-resolved vars and secrets (override if present) run: | # If the environment defines var-A/var-B, they override mapped var-1/var-2 values: if [ -n "${{ vars.var-1 }}" ]; then echo "var-1 available (env may override)"; fi if [ -n "${{ vars.var-2 }}" ]; then echo "var-2 available (env may override)"; fi # If the environment defines secret-A/secret-B, they override mapped secret-1/secret-2 values: if [ -n "${{ secrets.secret-1 }}" ]; then echo "secret-1 available (env may override)"; fi if [ -n "${{ secrets.secret-2 }}" ]; then echo "secret-2 available (env may override)"; fi
1 Declares the inputs accepted from the caller.
2 In job-rw-1, only the mapped vars (var-1, var-2) and mapped secrets (secret-1, secret-2) are available; other values from the caller are not. var-1 is also accessible via inputs.
3 Sets the job’s environment from env-prod, enabling environment-scoped resolution for both vars and secrets.
4 In job-rw-2, if the environment defines var-A/var-B or secret-A/secret-B, those values override the mapped caller values; otherwise, the mapped values are used.