,

A Developer’s Guide to GitHub Actions Inputs

Emmanuel Mumba avatar
A Developer’s Guide to GitHub Actions Inputs

If you want to build flexible and reusable CI/CD pipelines, you have to get comfortable with GitHub Actions inputs. They are the secret sauce for controlling your automation at runtime, letting you build powerful workflows without hardcoding values. In our experience, mastering inputs is what separates a brittle CI/CD setup from a truly scalable one.

Summary

  • Inputs are parameters that make your automation reusable, letting you pass values into custom actions and workflows at runtime.
  • There are two main types: Action Inputs, defined in action.yml, and Reusable Workflow Inputs, defined under on.workflow_call.
  • Action inputs configure a single task, while workflow inputs orchestrate a sequence of jobs.
  • You can enforce data types like string, boolean, and number to catch errors early.
  • Access input values using the ${{ inputs.input_id }} syntax and pass them using the with keyword.

Table Of Contents

A Quick Reference to GitHub Actions Inputs

Mastering GitHub Actions inputs is a game-changer for creating automation that scales. Instead of cloning nearly identical workflows for different environments say, one for staging and another for production you can build a single, robust workflow and just feed it different inputs when you run it.

This is how we build standardized CI/CD patterns that work across an entire organization. It empowers senior developers and tech leads to design automation that anyone can use without needing to dig into the nitty-gritty implementation details.

You’ll run into inputs in two main places:

  • Action Inputs: These are parameters for a custom action you’ve built, defined in an action.yml file. You pass them using the with keyword when you call the action in a workflow.
  • Reusable Workflow Inputs: These are parameters for an entire workflow that can be called by another one. You define them under the on.workflow_call trigger.

The diagram below gives you a high-level look at how these two input types compare.

GitHub Actions and Workflows inputs explained, detailing their definition and usage in a diagram.

The main takeaway here is that action inputs are for fine-tuning a single, granular task, while workflow inputs let you parameterize a whole sequence of jobs. To see this in action, check out our guide on setting up a CI/CD pipeline using GitHub Actions.

The following table breaks down the key differences, setting the stage for the more detailed examples we’ll dive into next.

Quick Guide to Input Types and Use Cases

This table provides a quick, side-by-side comparison of the two ways to define inputs in GitHub Actions, helping you decide which one to use based on your needs.

AspectAction Inputs (action.yml)Reusable Workflow Inputs (workflow_call)
Definition ContextInside the inputs section of an action.yml file for a custom action.Inside the on.workflow_call.inputs section of a reusable workflow file.
PurposeTo parameterize a single, self-contained unit of work (like a script or Docker container).To parameterize an entire workflow, which can contain multiple jobs and steps.
Typical Use CaseConfiguring a tool, like setting a version number, file path, or an API token for a step.Orchestrating a deployment by passing environment names, version tags, or feature flags.

In short, action.yml inputs control the what for a single step, while workflow_call inputs control the how for an entire automated process.

A Look Inside the action.yml Input Schema

When you’re building a custom action, the action.yml file is its beating heart. This file is where you define the action’s metadata, tell GitHub how to run it, and most importantly, declare the GitHub Actions inputs it accepts. A well-defined input schema is the first step toward building a robust and easy-to-use action.

You’ll declare every parameter your action can receive inside the inputs block. Each input is a key-value pair, where the key is the input’s ID and the value is an object spelling out its properties.

The Essential Input Properties

For pretty much any input you define, you’ll want to include three core properties. These are crucial for guiding users and making sure your action runs smoothly.

  • description: This is a clear, human-readable explanation of what the input actually does. This text shows up in the GitHub Actions UI, so it’s vital for anyone trying to use your action. Good descriptions prevent a lot of confusion.
  • required: A simple boolean (true or false) that specifies if a value absolutely must be provided. If you set this to true, any workflow that tries to run the action without this input will fail with an error.
  • default: This is a fallback value to use if the user doesn’t provide one. You can’t have an input that’s both required and has a default. Setting a sensible default makes your action much easier to use for common scenarios.

Here’s a quick example of these properties in action within an action.yml file.

# action.yml
name: 'Example Action'
description: 'An action demonstrating input properties'
inputs:
config-path:
description: 'The path to the configuration file.'
required: true
output-format:
description: 'The format for the output. Can be "json" or "text".'
required: false
default: 'json'

In this snippet, config-path is mandatory, forcing the user to point to a file. output-format, on the other hand, is optional; if the user omits it, it just defaults to 'json', simplifying their workflow setup.

Enforcing Data Types

Beyond the basics, GitHub Actions gives you a type keyword for some simple validation. This is a surprisingly powerful feature for catching bad data and preventing runtime errors before your action’s code even starts running.

You can set type to one of three values:

  • string: This is the default type if you don’t specify one. It accepts any text value.
  • boolean: Accepts true or false. GitHub is pretty flexible here and will correctly interpret values like true, True, 1, false, False, and 0.
  • number: Accepts both integers and floating-point numbers. Values like 10, 3.14, and -5 are all good to go.

Let’s expand on our previous example to include some type enforcement.

# action.yml (continued)
inputs:
# ... previous inputs
retries:
description: 'Number of times to retry a failed operation.'
type: number
default: 3
verbose:
description: 'Enable verbose logging for debugging.'
type: boolean
default: false

By defining retries as a number, you’re protecting your action’s logic from getting a non-numeric string like "three", which would almost certainly cause a script to crash. Likewise, verbose now expects a clear boolean, which makes conditional logic inside your action far more predictable.

Defining Inputs for Reusable Workflows

While custom actions are great for bundling specific, single tasks, reusable workflows are where you can standardize entire multi-job processes. This is an absolute game-changer for establishing consistent CI/CD patterns, like having a single deployment workflow that every microservice in your organization can call.

Defining GitHub Actions inputs for these workflows happens right inside the workflow file itself, using the on.workflow_call.inputs syntax. The structure feels a lot like the action.yml schema, but it lives directly in your reusable workflow’s YAML.

Reusable Workflow Input Syntax

The structure for laying out these inputs is pretty straightforward. You just list each input’s ID under on.workflow_call.inputs, followed by its properties. Just like with actions, you can set a description, a required status, a default value, and a type.

The available types give you some nice validation out of the box:

  • string: The default type if you don’t specify one.
  • number: For integers or floating-point values.
  • boolean: Perfect for simple true or false flags.
  • environment: A special type for passing a deployment environment name.
  • choice: Lets you define a specific list of allowed string values, which is great for preventing typos.

This strict schema is a lifesaver. It catches errors early by making sure any calling workflow provides valid data. If you want to see how this fits into the bigger picture, check out our guide on setting up a CI/CD pipeline using GitHub Actions.

A Practical End-to-End Example

Let’s walk through a real-world scenario. Imagine we have a reusable workflow that deploys an application. It needs an environment name, but also has optional inputs for a version tag and a dry-run flag.

First up, the reusable workflow itself, which we’ll save as .github/workflows/reusable-deploy.yml:

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy Workflow
on:
workflow_call:
inputs:
environment:
description: 'The deployment target environment'
required: true
type: string
version:
description: 'The version tag to deploy'
required: false
type: string
default: 'latest'
dry-run:
description: 'If true, runs a dry-run without deploying'
required: false
type: boolean
default: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to Environment
run: |
echo "Deploying version ${{ inputs.version }} to ${{ inputs.environment }}"
if [[ "${{ inputs.dry-run }}" == "true" ]]; then
echo "This is a dry run."
fi

Now, here’s how another workflow would call it and pass along the necessary inputs:

# .github/workflows/caller-workflow.yml
name: Deploy Web App
on:
push:
branches: [ main ]
jobs:
call-reusable-deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
version: v1.2.0

In this setup, the “caller” passes production for the environment and v1.2.0 for the version. The reusable workflow receives these values through the inputs context and uses them to run the right deployment steps. Since we didn’t provide a dry-run input, it automatically falls back to its default value of false. This clean separation is what makes building maintainable automation possible.

Passing and Accessing Input Values

Defining inputs in your action.yml or a reusable workflow is one half of the equation. The other, more critical half is actually using them. This is where the theory hits the road in your CI/CD pipelines.

To pass values into an action or reusable workflow, you’ll use the with keyword. This block is just a simple map of key-value pairs, where each key matches an input ID you defined.

jobs:
run-action:
runs-on: ubuntu-latest
steps:
- uses: my-org/my-custom-action@v1
with:
config-path: './deploy.json'
retries: 5

The good news is that the same syntax applies when you’re calling a reusable workflow. This consistency makes the API feel intuitive and easy to remember.

How to Access Inputs

So, how does the receiving action or workflow get the data? GitHub makes all the values you pass available through the inputs context object. You can grab any specific value with the expression syntax: ${{ inputs.your_input_id }}.

This is the standard, go-to method for referencing passed-in data. It doesn’t matter if you’re using it in a script, passing it as an argument to a tool, or evaluating it in a conditional if statement the inputs context is your entry point.

For instance, a reusable workflow can use an input to control which script to run:

# In reusable-workflow.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Run Build Script
run: |
echo "Building for environment: ${{ inputs.environment }}"
./build.sh --env ${{ inputs.environment }}

This simple mechanism is surprisingly powerful. It decouples the what from the how. The calling workflow only needs to know what environment to specify; it has no idea how the build script actually works.

Passing Dynamic Values

Sure, passing static strings is common, but the real magic of GitHub Actions inputs comes alive when you pass dynamic values. You can use any of GitHub’s context objects to pass information that changes with every single workflow run.

This pattern is the foundation for creating automation that truly adapts to its surroundings. It’s a great example of how understanding the flow of data is just as vital as knowing how to configure workflow triggers.

Here are a few common patterns you’ll see all the time:

  • Passing the Commit SHA: This is great for traceability, especially when deploying.


    with:
    commit-sha: ${{ github.sha }}
  • Using Step Outputs: You can easily chain steps together by using the output of one step as the input for the next.


    with:
    package-url: ${{ steps.build.outputs.url }}
  • Branch-Specific Logic: Pass the current branch name to control deployment logic.


    with:
    branch: ${{ github.ref_name }}

This ability to pass context-aware data is what makes your actions and workflows intelligent and highly reusable. It’s really the cornerstone of a scalable and maintainable CI/CD strategy.

Advanced Patterns for Complex Inputs

Once you’ve got the basics down, you can start digging into more powerful patterns for handling complex data and conditional logic. These are the kinds of techniques senior developers and architects rely on to build sophisticated, highly configurable CI/CD systems.

A common headache is trying to pass structured data, like a JSON object or a multi-line script, as a single input. At their core, GitHub Actions inputs are just strings, but you can easily work around this by passing the complex data as a string and then parsing it inside a script step.

Passing and Parsing JSON Objects

For instance, rather than defining a dozen separate inputs for deployment settings, you can bundle them all into a single JSON string. The popular command-line tool jq is practically made for this.

Here’s how you could pass a JSON object stuffed with deployment settings:

# In the caller workflow
jobs:
call-deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
deploy-config: '{ "region": "us-east-1", "instance_type": "t3.medium", "notify_channel": "#devops-alerts" }'

Then, inside the reusable workflow, a script step can crack open that JSON and put the values to work:

# In the reusable workflow
- name: Parse Config and Deploy
run: |
REGION=$(echo '${{ inputs.deploy-config }}' | jq -r '.region')
INSTANCE=$(echo '${{ inputs.deploy-config }}' | jq -r '.instance_type')
echo "Deploying to $REGION with instance type $INSTANCE..."

This pattern keeps your workflow’s with block tidy and makes your action’s interface much cleaner.

Conditional Logic with Optional Inputs

Another powerful technique is using if conditions on steps to create logic based on optional inputs. This lets you build workflows that can enable or disable entire chunks of functionality with a simple boolean flag.

Imagine a workflow where an optional run-tests input dictates whether a test suite gets executed:

# In action.yml or reusable-workflow.yml
inputs:
run-tests:
description: 'Set to true to run the full test suite.'
type: boolean
required: false
default: false

In the workflow itself, you just wrap the test step in a conditional that checks this input.

# In the job steps
- name: Run Integration Tests
if: ${{ inputs.run-tests == 'true' }}
run: npm test

A Note on Maintainability
As your actions get more complex, documentation becomes absolutely critical. Always provide a clear description for every input, choose sensible default values that cover the most common scenarios, and drop usage examples in your action’s README.

These constraints around inputs have real-world roots. For example, workflow_dispatch originally had a soft limit of 10 inputs, which pushed many teams toward workarounds like the JSON pattern. While that limit was eventually raised, it’s a great reminder of how input management directly impacts scalability. You can discover more insights about GitHub Actions inputs and see how they’ve evolved.

Troubleshooting Common Input Errors

Even when you’ve got a good handle on GitHub Actions inputs, things will inevitably break. It happens to everyone. A stray quote or a simple type mismatch is all it takes to grind your entire workflow to a halt.

When a workflow fails, the error messages aren’t always super helpful. Before you start guessing, the best first step is to see what values your action or reusable workflow is actually receiving. Our go-to move is to dedicate a single step just to printing the entire inputs context.

- name: Debug Inputs
run: echo "${{ toJson(inputs) }}"

By adding a simple step like this, you can dump the full JSON object of the inputs exactly as GitHub sees them. More often than not, this immediately reveals the root cause be it an unexpected string or a missing value.

Unpacking Data Type Mismatches

One of the most frequent culprits is a data type mismatch, and booleans are the usual suspect. A step with an if condition might be expecting a true boolean, but what it gets is the string "true".

That distinction is everything. GitHub’s expression engine is strict; the boolean true is not the same thing as the string "true".

To fix this, make sure you’re passing booleans without quotes in your with block and checking for the correct type in your conditionals.

  • Incorrect (passes a string): with: deploy: "true"
  • Correct (passes a boolean): with: deploy: true
  • Correct Conditional Check: if: ${{ inputs.deploy == true }}

Being precise with your data types will save you from a whole class of subtle bugs in your workflows.

Solving Syntax and Context Issues

Another common pitfall is a syntax error in the with block or trying to access the inputs context where it simply doesn’t exist. You have to remember, the inputs context is only available within a reusable component that is, a custom action or a called workflow.

If you ever see an error like “Unrecognized named-value: ‘inputs’”, it’s a dead giveaway that you’re trying to use ${{ inputs.some_input }} in a place that doesn’t have it, like a standard, non-reusable workflow job.

Here are a few quick checks to run through:

  • Verify Indentation: YAML is notoriously picky about spacing. Make sure your with block and all its key-value pairs are correctly indented.
  • Quote Appropriately: If an input value has special characters or spaces, wrap it in single or double quotes. This ensures it’s passed along as a single, intact string.
  • Check Context Availability: Double-check that you’re actually inside a reusable workflow or a custom action before you try to access the inputs context.

Running through these debugging steps can turn a frustrating failure into a quick fix, helping you build more resilient and predictable CI/CD pipelines.

When you share an action or reusable workflow, your documentation isn’t just an afterthought it’s a core feature. Keeping your GitHub Actions inputs clearly documented is a must-have for team collaboration and open-source adoption. But as an action’s logic evolves, its inputs inevitably change.

This is where documentation drift begins. Manually keeping a README file in sync with your action.yml is tedious and a classic source of error. When the docs don’t match the code, you get user frustration, failed workflow runs, and hours of wasted developer time.

The Challenge of Documentation Drift

GitHub Actions has seen explosive growth since its 2018 launch. As teams build more complex CI/CD pipelines, the ability to correctly configure workflow inputs is critical for keeping things moving. This rapid adoption makes accurate, up-to-date documentation more important than ever. You can read more about this growth on GitHub’s blog.

When documentation falls out of sync, the pain is immediate:

  • Failed Pipelines: Users copy-paste outdated examples, and their workflows break.
  • Incorrect Usage: Teams misuse inputs because the descriptions are wrong, leading to unexpected behavior.
  • Maintenance Burden: You end up spending more time answering questions and debugging user issues than actually improving the action.

At this point, automated tooling stops being a nice-to-have and becomes a necessity.

Continuous Documentation with DeepDocs

A tool like DeepDocs can eliminate documentation drift entirely. It acts as a CI/CD pipeline for your documentation, ensuring your README always reflects the ground truth defined in your action.yml.

DeepDocs plugs right into your repository and keeps an eye on your files. When it detects that your input definitions no longer match the documentation, it springs into action.

By automatically generating pull requests with the necessary doc updates, DeepDocs ensures that your action’s public interface is always accurately represented. This prevents user confusion and builds trust in the automation you provide.

It’s smart enough to update only the parts of your documentation that are out of sync, preserving all your existing formatting and style. For engineering leads and open-source maintainers, this means you can refactor your action’s inputs with confidence, knowing the documentation will be updated automatically. This turns documentation from a manual chore into a reliable, automated process.

Common Questions (And Expert Answers) About Inputs

As you get deeper into building actions and reusable workflows, a few common questions always seem to pop up. Let’s tackle them head-on with some practical, no-nonsense answers.

What’s the Real Limit on the Number of Inputs?

This is a classic “it depends” situation, and knowing the context is everything.

  • For your own custom actions (in an action.yml file), there’s no official hard limit. But we’d advise against going wild.
  • The limits kick in for workflow_dispatch (manual triggers) and workflow_call (reusable workflows). GitHub recently bumped the workflow_dispatch limit from 10 to 25, which was a huge relief for anyone building complex deployment pipelines. You can see the official word in the GitHub Changelog.

As a rule of thumb, if you find yourself needing more than a dozen inputs, consider passing a single JSON string and just parsing it inside your script. It keeps your workflow files readable and your action much easier to maintain.

How Do I Handle Inputs with Spaces or Special Characters?

This is a big one, and getting it wrong leads to frustrating, hard-to-debug errors. The secret is simple: quote everything, everywhere.

First, when you pass the value in your workflow YAML, wrap it in quotes. This tells the YAML parser it’s a single string.

- uses: my-org/my-action@v1
with:
message: "This is a message with spaces!"

But the most critical step is inside your action’s script. When you use the input, you must quote the variable expansion. This stops the shell from splitting the string into separate arguments.

# In your action's script
echo "${{ inputs.message }}"

Without those quotes around ${{ inputs.message }}, the echo command would see multiple arguments, and that’s how perfectly good scripts fall apart.

Can Input Definitions Be Generated Dynamically?

Nope. The input definitions the actual schema you write in action.yml or on.workflow_call.inputs have to be static. Think of it as the public API for your action or workflow; it can’t change at runtime. This is fundamental to how the GitHub Actions runner works, as it needs to validate calls before anything starts running.

What you can do, and what you’ll do all the time, is dynamically generate the values you pass to those inputs. The most common pattern is to use the output of one step as the input for the next, creating a data flow right inside your job.

- name: Setup Environment
id: setup
run: echo "path=./configs/prod.json" >> $GITHUB_OUTPUT
- name: Deploy
uses: my-org/deploy-action@v1
with:
config_path: ${{ steps.setup.outputs.path }}

This lets your workflows react to what’s happening at runtime, making them far more powerful.

What’s the Difference Between inputs and env?

It’s easy to mix these up, but they serve two very different purposes. Confusing them often leads to a messy design.

  • inputs are for parameterization. They are the formal, public API of your action or reusable workflow.
  • env is for configuring the execution environment. It sets environment variables that are available to any script or process running within a particular step or job.

Of course, you can use an input to set an environment variable (e.g., env: MY_VAR: ${{ inputs.my_input }}), but their roles are still distinct. Here’s the cleanest way to think about it: inputs define the contract, and env is part of the implementation.

Manually updating the documentation for your action’s inputs is a losing battle. Code changes, inputs get added or removed, and your README slowly turns into a work of fiction. DeepDocs is the automated documentation engineer you’ve been missing. It watches for changes in your action.yml and automatically opens pull requests to keep your README perfectly in sync. Stop letting your docs drift and give your users the accurate, reliable information they need. See how it works at https://deepdocs.dev.

Leave a Reply

Discover more from DeepDocs

Subscribe now to keep reading and get access to the full archive.

Continue reading