How to Send More Than Ten Inputs with Workflow Dispatch

There is a limit of ten inputs on the number of inputs you can send when dispatching a GitHub workflow.

  • The maximum number of top-level properties for inputs is 10.

However, with highly parameterized workflows, I quickly hit the limit. In my case, I had a Cypress workflow with the following inputs:

on:
  workflow-dispatch:
    inputs:
      name: ... #nice test name in workflow RUN UI
      php_version: ...
      db_version: ... #specific version of maria/mysql/percona
      db_type: ... #maria/percona/mysql
      mailpit_version: ... #version of the docker image with mailpit
      template: ... # which app template to use in cypress?
      project: ... # which project to setup for cypress test
      split: ... # to how many jobs split cypress test suite
      record_video: ... # record vido of cypress tests
      flaky_report: ... # report flaky tests?
# 10 inputs in total

I needed to add one more input, send_slack_notification: true/false, to be able to disable Slack notification sending and do it from elsewhere (parent workflow).

For dispatching, as usual, I was using my favorite dispatch action workflow-dispatch-and-wait. However, I was facing several major issues with it:

  • It didn’t help with my ten inputs limit problem.

  • Setting many inputs in JSON is less readable and complex to extend.

  • Action was not connecting the dispatching and dispatched workflows in any way (see my previous article How to connect dispatching and dispatched workflows about this topic).

Fortunately, the action is very simple and easy to extend. So, I extended it to solve all my problems mentioned above:

  • To have more than 10 inputs (up to 65k payload limit, more on that later).

  • Use YAML in YAML (yeah, sounds crazy but far better than serialized JSON) for writing inputs - improved readability.

  • Dispatcher automatically send meta into dispatched workflow - no more extra steps to prepare the data.

Let’s break it down to three simple steps.

Allow Sending More Than 10 Inputs

My dispatching workflow originally looked like this:

#...snip
jobs:
  run-cypress:
    runs-on: ubuntu-latest
      - name: Start cypres tests
        uses: the-actions-org/workflow-dispatch@v4
        with:
          workflow: cypress-run.yaml
          token: ${{ secrets.GITHUB_TOKEN }}
# max. 10 inputs here
# had to be json 🤮
          inputs: >-
            {
              "name": "Cypress dark",
              "php_version": "8.3",
              "db_version": "10.11.8",
              "db_type": "mariadb",
              "mailpit_version": "latest",
              "template": "dark",
              "project": "cy_test_prj1",
              "split": "2",
              "record_video": "false",
              "flaky_report": "true"
            }

What I did to support more inputs was reserve one input named meta and put all extra inputs there.

My dispatching workflow would now look like this:

#...snip
jobs:
  run-cypress:
    runs-on: ubuntu-latest
      - name: Start cypres tests
        uses: the-actions-org/workflow-dispatch@v4
        with:
          workflow: cypress-run.yaml
          token: ${{ secrets.GITHUB_TOKEN }}
# max. 10 inputs here
          inputs: >-
            {
              "name": "Cypress dark",
              "php_version": "8.3",
              "db_version": "10.11.8",
              "db_type": "mariadb",
              "mailpit_version": "latest",
              "template": "dark",
              "project": "cy_test_prj1",
              "split": "2",
              "record_video": "false",
# if using JSON, meta's value have to be stringified 🤮
              "meta": "{\"flaky_report\": \"true\", \"send_slack_notification\":\"false\"}"
            }

Of course, one input named meta in the dispatched workflow has to be reserved to receive the data:

on:
  workflow_dispatch:
    inputs:
      #...other inputs
      meta:
        description: 'Meta information to pass to the workflow. JSON string.'
        required: false
        default: ''

Later on, in the dispatched workflow, you can easily decode whatever you need from the meta input using the fromJSON expression like this:

steps:
  - name: Notify slack
    if: ${{ fromJSON(inputs.meta).send_slack_notification == 'true' }}
    run: |
      slack-notify-cli --channel=info --message="Workflow successful"

Now I can send as many inputs to my dispatched workflows as I want 🎉!

The meta field is stringified so I can conveniently work with it using the fromJSON expression.

With this convenience, I introduced a new issue. Data in this meta field now have to be always serialized JSON. Which is hard do maintain and read.
# meta field must be always encoded JSON 😢
"meta": "{\"flaky_report\": \"true\", \"send_slack_notification\":\"false\"}"

This might be fine for one or two extra inputs, but it is not manageable or readable for anything more than that.

YAML to the Rescue

I realized that, with a little help from the yaml npm library, I can rewrite my dispatching workflow to look like this:

#...snip
jobs:
  run-cypress:
    runs-on: ubuntu-latest
      - name: Start cypres tests
        uses: the-actions-org/workflow-dispatch@v4
        with:
          workflow: cypress-run.yaml
          token: ${{ secrets.GITHUB_TOKEN }}
          inputs: |
            name: Cypress dark
            php_version: 8.3
            db_version: 10.11.8
            db_type: mariadb,
            mailpit_version: latest
            template: dark
            project: cy_test_prj1
            split: 2
            record_video: false
            meta:
              flaky_report: true
              send_slack_notification: false

Much cleaner, I think:

  • no more unnecessary quotations around keys or values

  • no JSON at all

  • as many inputs under the meta key as I want

I only needed to change the action’s parse method to this (slightly simplified version):

function parse(inputsJsonOrYaml: string) {
  if(inputsJsonOrYaml) {
    //let's try to parse JSON first
    try {
      const parsedJson = JSON.parse(inputsJsonOrYaml)
      //okay it was a valid JSON
      return parsedJson
    } catch(e) {
      core.debug(`Failed to parse inputs as JSON: ${(e as Error).message}`)
      //original method would end here with error
    }
    //ok, it wasn't a valid JSON, let's try again with YAML
    const parsedYaml = YAML.parse(inputsJsonOrYaml)
    if (typeof parsedYaml !== 'object') {
      //inputs must be an object, otherwise it doesn't make sense
      const error = new TypeError('Failed to parse \'inputs\' parameter. Must be a valid JSON or YAML.');
      core.setFailed(error)
      throw error
    }
    core.debug('Inputs parsed as YAML')
    //ok inputs were parsed as YAML
    //stringify inputs internally, because those are sent
    //to the dispatched workflow through REST call
    parsedYaml.meta = JSON.stringify(parsedYaml.meta)
    return parsedYaml
  }
  return {}
}

Connecting Dispatching and Dispatched Workflow

The last thing that annoyed me was that the dispatched workflow didn’t know which workflow dispatched it.

I wrote about how this can be solved in a previous article of this series called How to connect dispatching and dispatched workflows.

The only problem was that I had to manually pass the data when dispatching:

#...snip
jobs:
  run-cypress:
    runs-on: ubuntu-latest
      - name: Start cypres tests
        uses: the-actions-org/workflow-dispatch@v4
        with:
          workflow: cypress-run.yaml
          token: ${{ secrets.GITHUB_TOKEN }}
          inputs: |
            name: Cypress dark
            php_version: 8.3
            db_version: 10.11.8
            db_type: mariadb,
            mailpit_version: latest
            template: dark
            project: cy_test_prj1
            split: 2
            record_video: false
            meta:
# always pass following 3 inputs :(
              workflow-name: ${{github.workflow}}
              workflow-url: ${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}/attempts/${{github.run_attempt}}
              workflow-repo: ${{github.repository}}
              another-key: another value

So I extended my action again to handle it automatically. Again all that was needed, to slightly update the parse method (simplified version):

function parse(inputsJsonOrYaml: string) {
  if(inputsJsonOrYaml) {
    //let's try to parse JSON first
    try {
      const parsedJson = JSON.parse(inputsJsonOrYaml)
      //okay it was a valid JSON
      core.debug('Inputs parsed as JSON')
      return parsedJson
    } catch(e) {
      core.debug(`Failed to parse inputs as JSON: ${(e as Error).message}`)
    }
    //ok, it wasn't a valid JSON, let's try again with YAML
    const parsedYaml = YAML.parse(inputsJsonOrYaml)
    if (typeof parsedYaml !== 'object') {
      //inputs must be an object, otherwise it doesn't make sense
      const error = new TypeError('Failed to parse \'inputs\' parameter. Must be a valid JSON or YAML.');
      core.setFailed(error)
      throw error
    }
    core.debug('Inputs parsed as YAML')
    //ok inputs were parsed as YAML
    if (!parsedYaml.meta) {
      //if there was no `meta` input, initialize it
      parsedYaml.meta = {}
    }
    //add info about self, for dispatched workflow
    parsedYaml.meta.workflow_name = github.context.workflow
    parsedYaml.meta.workflow_url = `${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/actions/runs/${github.context.runId}/attempts/${parseInt(process.env.GITHUB_RUN_ATTEMPT as string)}`
    parsedYaml.meta.workflow_repo = `${github.context.repo.owner}/${github.context.repo.repo}`

    //stringify inputs back, 
    //so I can work with with fromJSON expression
    parsedYaml.meta = JSON.stringify(parsedYaml.meta)
    return parsedYaml
  }
  return {}
}

Now I can simply dispatch like:

#...snip
jobs:
  run-cypress:
    runs-on: ubuntu-latest
      - name: Start cypres tests
        uses: the-actions-org/workflow-dispatch@v4
        with:
          workflow: cypress-run.yaml
          token: ${{ secrets.GITHUB_TOKEN }}
# finally clean & clear 🥳
          inputs: |
            name: Cypress dark
            php_version: 8.3
            db_version: 10.11.8
            db_type: mariadb,
            mailpit_version: latest
            template: dark
            project: cy_test_prj1
            split: 2
            record_video: false
            meta:
              send_slack_notification: false

And get all the data I need in the dispatched workflow with the fromJSON expression like this:

name: Some dispatched workflow

on:
  workflow_dispatch:
    inputs:
      #...mx. 9 primary inputs...
      meta:
        description: 'Meta information to pass to the workflow. JSON string'
        required: false
        default: ''

jobs:
  echo:
    runs-on: ubuntu-latest
    steps:
      # display dispatching workflow info and connecting link
      - name: Parent info
        if: ${{ inputs.meta != '' && fromJSON(inputs.meta).workflow_name != '' }}
        shell: bash
        run: |
          echo 'Dispatched from [${{ fromJSON(inputs.meta).workflow_name }}](${{ fromJSON(inputs.meta).workflow_url }}) in repo `${{ fromJSON(inputs.meta).workflow_repo }}`' >> $GITHUB_STEP_SUMMARY

      # read any value from meta
      - name: Parse meta to outputs
        if: ${{ inputs.meta != '' && fromJSON(inputs.meta) != '' }}
        id: meta
        shell: bash
        run: |
          echo "Extra inputs from meta:" >> $GITHUB_STEP_SUMMARY
          echo '- some key: `${{ fromJSON(inputs.meta).send_slack_notification }}`'  >> $GITHUB_STEP_SUMMARY
      - name: Send slack notification
        if: ${{ fromJSON(inputs.meta).send_slack_notification == 'true' }}
        uses: some/action
One thing to notice here is that if we’re using YAML to send the inputs, all parsed values will be strings.

You can find the complete extended action repository on my GitHub: roamingowl/workflow-dispatch-with-yaml-support.

Conclusion

With the simple extension of the original dispatch action workflow-dispatch-and-wait, I was able to bypass the dispatch inputs count limit and improve my workflows' readability.

There is a catch, though. The Inputs field size can’t exceed the 65k payload limit. So this method is not ideal for transferring large data between workflows. I’ll address this problem next time (and how to solve it).

That is all for now. Thank you. 👋

Happy coding :)