Skip to main content
GitHub Actions · lesson 4 of 7
GitHub Actions intermediate Lesson 4 of 7

Reusable Workflows: Real Input Passing, and a required Validation Gap in act

A workflow_call input flows through correctly when supplied — but marking it required: true doesn't stop act from running the job anyway with a silently empty value when it's omitted.

By SkillByExample · Updated

github-actions reusable-workflows act workflow-call

A reusable workflow takes real inputs from its caller — but one of its safety guarantees doesn’t survive local testing with act the way it should.

A reusable workflow, called with a real input

# .github/workflows/reusable.yml
name: Reusable
on:
  workflow_call:
    inputs:
      target:
        required: true
        type: string
jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - run: echo "deploying to ${{ inputs.target }}"
# .github/workflows/caller.yml
name: Caller
on: [push]
jobs:
  call-it:
    uses: ./.github/workflows/reusable.yml
    with:
      target: staging
act push -W .github/workflows/caller.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[call-it/Reusable/greet] ⭐ Run Set up job
[call-it/Reusable/greet]   ✅  Success - Set up job
[call-it/Reusable/greet] ⭐ Run Main echo "deploying to staging"
[call-it/Reusable/greet]   | deploying to staging
[call-it/Reusable/greet]   ✅  Success - Main echo "deploying to staging" [126.928656ms]
[call-it/Reusable/greet] 🏁  Job succeeded

The job name call-it/Reusable/greet shows the full chain: the caller’s job (call-it), the called workflow’s name (Reusable), and its job (greet) — target: staging flowed straight through into inputs.target inside the called workflow.

The gap: required: true isn’t actually enforced by act

# .github/workflows/caller_missing.yml
name: CallerMissing
on: [push]
jobs:
  call-it:
    uses: ./.github/workflows/reusable.yml

No with: block at all — the required target input is simply never supplied.

act push -W .github/workflows/caller_missing.yml -P ubuntu-latest=catthehacker/ubuntu:act-latest
[call-it/Reusable/greet] ⭐ Run Set up job
[call-it/Reusable/greet]   ✅  Success - Set up job
[call-it/Reusable/greet] ⭐ Run Main echo "deploying to "
[call-it/Reusable/greet]   | deploying to 
[call-it/Reusable/greet]   ✅  Success - Main echo "deploying to " [149.864147ms]
[call-it/Reusable/greet] 🏁  Job succeeded

required: true should have rejected this call outright — on real GitHub Actions, workflow_call inputs are validated before any job is even scheduled, and a missing required input fails the run immediately with a clear error. Here, act let it through, silently resolved inputs.target to an empty string, and the job reported succeeded while actually deploying to nowhere. This is a real, observed gap in act’s local emulation of workflow_call — not something GitHub’s real service is documented to do.

Practice

1. Add an explicit runtime check that fails loudly instead of trusting required: true alone.
jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Check target is set
        run: |
          if [ -z "${{ inputs.target }}" ]; then
            echo "::error::target input is required but was empty"
            exit 1
          fi
      - run: echo "deploying to ${{ inputs.target }}"
[call-it/Reusable/greet] ⭐ Run Main Check target is set
[call-it/Reusable/greet]   ❗  ::error::target input is required but was empty
[call-it/Reusable/greet]   ❌  Failure - Main Check target is set [263.652124ms]
[call-it/Reusable/greet] 🏁  Job failed
Error: Job 'greet' failed

Now the missing-input case actually fails, under act and on real GitHub Actions alike — ::error:: is GitHub’s own log-annotation syntax for surfacing a message as a workflow error, visible even without opening the raw log.

2. Pass an unexpected extra input the reusable workflow never declared.
jobs:
  call-it:
    uses: ./.github/workflows/reusable.yml
    with:
      target: staging
      region: us-east-1
[call-it/Reusable/greet] ⭐ Run Main echo "deploying to staging"
[call-it/Reusable/greet]   | deploying to staging
[call-it/Reusable/greet]   ✅  Success - Main echo "deploying to staging" [105.432526ms]
[call-it/Reusable/greet] 🏁  Job succeeded

region was never declared in the called workflow’s on.workflow_call.inputs block, and act didn’t reject it — it just silently dropped the unrecognized input and ran normally with target still working correctly. This is the same pattern as the missing-required-input gap: act’s workflow_call input validation is looser than GitHub’s real service, so a typo’d input name here would not be caught locally either.

3. Add a second declared input with a default value and omit it from the call.
# reusable.yml, updated
inputs:
  target:
    required: true
    type: string
  dry_run:
    required: false
    type: boolean
    default: false
[call-it/Reusable/greet]   | deploying to staging, dry_run=false

Omitting an optional input with a declared default correctly falls back to that default — this is the one case where “input not supplied” behaves exactly as documented, unlike the required: true case above.

Next: deployment environments, and a real limitation this substrate has for approval gates.

Frequently Asked Questions

What makes a workflow "reusable" versus a normal workflow?
A workflow becomes callable by other workflows the moment it declares on: workflow_call — that trigger is what exposes it as something a job elsewhere can invoke with uses:, the same way a job invokes a third-party action, except the callee here is another workflow file in the same (or a different) repository.
How does a required input actually get validated on real GitHub Actions?
GitHub validates workflow_call inputs before the called workflow's jobs are scheduled at all — a missing required: true input fails the calling job immediately with a clear error naming the missing input, before any runner is even provisioned. This lesson's finding is specifically that act does not reproduce this pre-flight validation.
Why did the workflow succeed instead of failing when a required input was omitted?
act let the call through and simply resolved the missing input to an empty string inside the called workflow, rather than rejecting the call at parse time the way GitHub's real workflow_call validation does. The job ran to completion and reported success with a silently wrong value ("deploying to " instead of a real target) rather than failing loudly.
Should I rely on required: true for correctness if I'm testing locally with act?
Not for catching a missing input specifically — that validation gap means a broken caller can look like it works under act and then fail (or worse, silently misbehave) only when it actually reaches GitHub. Add an explicit runtime check inside the called workflow (a step that fails if the input variable is empty) if this failure mode matters, rather than trusting required: true alone during local testing.
Can a reusable workflow be called from a different repository?
Yes — uses: owner/repo/.github/workflows/file.yml@ref can reference a workflow in another repository at a specific ref (branch, tag, or SHA), the same versioning model as calling a third-party action. A local uses: ./.github/workflows/file.yml (as in this lesson) only works within the same repository.