Skip to content
Latchkey LogoLatchkey home

GitHub Actions unable to interpret as boolean: what it really says

GitHub Actions unable to interpret as boolean is a message we can find in no part of the product that could emit it, and searching for it is usually a search for one of three real problems. Two of those raise the same error at different moments, one before the run and one during it, and the third raises nothing at all, which is why it is the one that reaches production.

Five boolean values in a workflow and the different outcome each one produces
The parser recognizes only true, True, TRUE, false, False and FALSE as booleans. Everything else is a string, and a non-empty string is truthy.

What this error means

You set a key that takes true or false, and either the run refuses to start with wording you did not expect, or a step that has already failed takes the job down with a second error about the value rather than the type, or everything goes green and behaves as though you had written the opposite. The last shape is the dangerous one: a step guarded by a checkbox input runs when the box is unticked, a continue-on-error you set to a variable swallows a real failure, and nothing anywhere is red. The workflow is valid, the expression evaluates, and the value it produced was a string the whole time.

Quoted from actions/runner#2418, a continue-on-error expression that produced no boolean
Error: .github/workflows/test.yml (Line: 24, Col: 28): Unexpected value ''
Error: The step failed and an error occurred when attempting to determine whether to continue on error.
Error: The template is not valid. .github/workflows/test.yml (Line: 24, Col: 28): Unexpected value ''

Where the phrase does not come from

We looked for this string in the three places that could produce it and did not find it. The workflow schema and the template reader that validates against it live in actions/runner under src/Sdk/WorkflowParser. The expression parser lives in the same repository under src/Sdk/DTExpressions2. The editor and language server that surface the same errors before you push live in actions/languageservices. Searching all three for "Unable to interpret" returns nothing.

A search that cannot fail is not evidence, so we checked the instrument on the same trees. "Unrecognized named-value" appears in ten files and "Unexpected symbol" in twelve, both of which are messages this parser really emits, so the search does reach the strings it is looking for. A nonsense phrase returned zero, so it can also return zero. Twenty-four issue-search results for the phrase paired with "github actions" were all from Pydantic, TypeDoc and unrelated configuration loaders, none of them a workflow error.

One caveat we cannot close: the service that validates a workflow before a run starts is not public, and it is not impossible that it carries wording the shipped parser does not. What we can say is that nothing in the code that the runner, the language server and the VS Code extension all share produces this sentence, and that every real report we found of a boolean type mismatch quotes one of the two messages below instead.

Common causes

The value came from github.event.inputs rather than inputs

The most common of the three by a distance, and the only one with no error text. The event payload carries every dispatch input as a string, so a false checkbox arrives as "false" and evaluates as true. Code that worked when the input was a string type breaks silently the day someone declares it type: boolean, because the two contexts diverge at exactly that point.

A literal that YAML 1.2 does not call a boolean

Writing yes, on or a quoted 'true' in a key that takes a boolean. The reader hands the schema a string, the schema wanted a boolean, and the template reader reports the value back to you as unexpected. This is caught before the run starts, so it costs a push rather than a deployment.

An expression that returned a string

Interpolating a variable, a step output or a vars value into a boolean key. Everything written to $GITHUB_OUTPUT is a string, and so is every repository variable, so continue-on-error: ${{ steps.probe.outputs.ok }} evaluates to a string token, which the boolean definition for that key refuses as an unexpected value. Because the key is only read after the step has failed, this one surfaces in the middle of a run rather than at validation.

A reusable workflow input typed as a string at one end

An input declared type: boolean in the called workflow but passed from a caller value that is a string, or the reverse. The call boundary is where the declared type is applied, so a mismatch surfaces at the call rather than where the value was written.

How to fix it

Read dispatch inputs through the inputs context

Replace github.event.inputs.<name> with inputs.<name> everywhere in a workflow_dispatch or workflow_call workflow. The two contexts carry the same information, and only the second one keeps a declared boolean as a boolean. This is a search and replace, and it is the whole fix for the silent case.

.github/workflows/deploy.yml (illustrative)
      - name: Deploy
        if: inputs.deploy
        run: ./scripts/deploy.sh

Compare, rather than coerce, when the value really is a string

When the value comes from a step output, a repository variable or an environment variable, it is a string and will stay one. Turn it into a boolean with an explicit comparison, which returns a real boolean from the expression engine rather than relying on truthiness. Comparing against the exact string you wrote is also self-documenting for the next reader.

.github/workflows/ci.yml (illustrative)
      - name: Publish
        if: steps.probe.outputs.ok == 'true'
        run: ./scripts/publish.sh

Write boolean literals in one of the six accepted spellings

  1. Use true or false, unquoted, in any key the schema types as a boolean.
  2. Do not write yes, no, on or off and expect them to be read as booleans.
  3. Do not quote a boolean literal: quoting it makes it a string.
  4. If your editor or linter rewrites these, check it is not applying YAML 1.1 rules the Actions parser does not follow.

Declare the type on both sides of a reusable workflow

Give the input type: boolean in the called workflow and pass a real boolean from the caller. Passing a vars value or a step output through with: sends a string across the boundary no matter what the callee declares, so convert it with a comparison in the caller before it crosses.

.github/workflows/ci.yml (illustrative)
  call:
    uses: ./.github/workflows/build.yml
    with:
      publish: ${{ github.ref == 'refs/heads/main' }}

What a boolean key really says when you give it a string

It is one error raised at two different moments, and which you meet depends on whether the bad value was a literal or the result of an expression. A literal is checked by the template reader: when the scalar does not match the boolean definition in the schema, TemplateReader.Validate falls through to m_context.Error(literal, TemplateStrings.UnexpectedValue(literal)), and UnexpectedValue is defined in TemplateStrings.resx as "Unexpected value '{0}'". So continue-on-error: yes is reported as an unexpected value, with the offending token quoted back at you.

An expression takes the other path, and it takes it late. A step continue-on-error is only read once the step has already failed: ApplyContinueOnError in ExecutionContext.cs returns immediately unless the result is a failure, and only then calls EvaluateStepContinueOnError. That method evaluates the expression, turns the result into a token, and validates the token against the schema definition for the key, which is boolean: {} and nothing else. BooleanDefinition.IsMatch returns true only for a BooleanToken, so a result that came back a string, or came back with no value at all, fails the same unexpected-value check a bad literal fails, and context.Errors.Check() throws before the converter under it is ever reached.

That is the error quoted at the top of this page, captured by a reporter whose expression produced no boolean. Two things about it are worth keeping. It carries the same wording as the literal case, so the message alone does not tell you which of the two you have: the moment tells you, because this one arrives mid-run and brings a second line naming what the runner was trying to decide. And it says nothing about booleans or token types, which is worth remembering the next time you meet a message that does.

actions/runner, src/Sdk/WorkflowParser/WorkflowTemplateEvaluator.cs, EvaluateStepContinueOnError
token = TemplateEvaluator.Evaluate(context, WorkflowTemplateConstants.BooleanStepsContext, token, 0, null);
context.Errors.Check();
result = WorkflowTemplateConverter.ConvertToStepContinueOnError(context, token);

The BooleanToken message is your editor, not the runner

Search this phrase and a third message turns up: "Unexpected type 'BasicExpressionToken' encountered while reading 'steps item continue-on-error'. The type 'BooleanToken' was expected." It is a real message and it is not the runner's. The runner builds its own description for that key as $"step {WorkflowTemplateConstants.ContinueOnError}", which renders step continue-on-error; the wording with steps item in it appears in no file anywhere in actions/runner. The one place it does appear is the TypeScript workflow parser behind the VS Code extension.

The token name settles it independently. A BasicExpressionToken is an expression that has not been evaluated yet, and the runner never asserts a boolean on one: the parse-time call returns early for any expression token, and the run-time call evaluates before it asserts. Only a static analyzer, reading the file without running it, can hold an unevaluated expression and demand a boolean of it.

That was a bug, not a diagnosis. It was reported as github/vscode-github-actions#33 by someone whose input really was declared type: boolean, and the maintainer named it in one line: the extension was converting the field to a boolean rather than allowing it to contain expressions. It was fixed in 2023 and the call is now guarded, so the workflow in that report runs, and the fourth row of the table below is that workflow. If this message is on your screen today, check whether it came from a run or from the problems panel.

actions/languageservices, workflow-parser/src/model/converter/steps.ts, guarded since the 2023 fix
case "continue-on-error":
  if (!item.value.isExpression) {
    continueOnError = item.value.assertBoolean("steps item continue-on-error").value;
  } else {
    continueOnError = item.value.assertScalar("steps item continue-on-error");
  }

Only six spellings are booleans

The YAML reader the workflow parser uses follows the YAML 1.2 core schema rather than the older 1.1 rules most people learned. MatchBoolean in YamlObjectReader.cs accepts exactly true, True, TRUE, false, False and FALSE and returns false for everything else, with a comment in the source naming the spec it is following. So yes, no, on, off, y, n, 1 and 0 are all plain strings here, whatever your editor colors them.

That matters twice over. A quoted 'true' is a string too, and so is anything an expression produced unless the expression genuinely returned a boolean. The table below sets the five values side by side, in the order you are most likely to meet them.

What you wroteHow the parser reads itWhat happens
continue-on-error: truea booleanworks
continue-on-error: yesthe string "yes"rejected as an unexpected value
continue-on-error: 'true'the string "true"rejected as an unexpected value
continue-on-error: ${{ inputs.flag }} with type: booleana booleanworks
if: ${{ github.event.inputs.flag }}the string "false" or "true"always truthy, never an error

The silent one, and the context that fixes it

The last row is the case that costs real time, because there is no message to search for. A workflow_dispatch input declared type: boolean arrives in two places at once, and only one of them keeps the type. The documentation is precise about it: "The information in the inputs context and github.event.inputs context is identical except that the inputs context preserves Boolean values as Booleans instead of converting them to strings."

A string of "false" is not falsy in this expression language. EvaluationResult.IsFalsy in the runner treats a string as false only when it equals the empty string, so "false" is truthy and the step you guarded runs. This was reported as actions/runner#1483, "Boolean inputs are not actually booleans", against a workflow that reads github.event.inputs.foo in a step condition; the inputs context is the answer to it.

.github/workflows/deploy.yml, corrected (illustrative)
on:
  workflow_dispatch:
    inputs:
      deploy:
        type: boolean
        default: false

jobs:
  ship:
    if: inputs.deploy
    runs-on: ubuntu-latest
    steps:
      - run: ./scripts/deploy.sh

Why there is no recorded run on this page

The main claim on this page is a negative, and a run cannot demonstrate a negative. No number of green jobs would show that a message is never emitted; only reading every branch that could emit one does that, which is what the section above describes and what the code quotation supports. The two real errors are raised before a job exists, so there would be no runner log to capture even if we wanted one. The third case produces a green run whose log is indistinguishable from a correct run, which is exactly why it is worth a page.

How to prevent it

  • Treat github.event.inputs as string-only and reach for the inputs context instead.
  • Assume anything written to $GITHUB_OUTPUT or stored in vars is a string.
  • Compare with == when you need a boolean out of a value you did not declare.
  • Run actionlint in CI: it knows which keys are typed and which contexts are available.

Frequently asked questions

Does GitHub Actions really emit "Unable to interpret as boolean"?
We could not find it. The string appears nowhere in the workflow parser, the expression parser or the language services that share the same error text, while control searches for real messages in those trees succeed. Every report we could find of a boolean type mismatch quotes "Unexpected value" from the runner, or the "The type 'BooleanToken' was expected" wording that the VS Code extension used to emit for a workflow that runs correctly.
Why does my if condition run when a boolean input is false?
Because you are almost certainly reading github.event.inputs, which converts booleans to strings. The expression engine treats a string as false only when it is empty, so the string "false" is truthy. Read inputs.<name> instead, which preserves the declared boolean type.
Is yes the same as true in a workflow file?
No. The Actions YAML reader follows the YAML 1.2 core schema, which recognizes only true, True, TRUE, false, False and FALSE. yes, no, on and off are read as ordinary strings, so a key that requires a boolean rejects them.
How do I pass a boolean into continue-on-error?
Either write the literal true or false, or use an expression that genuinely returns a boolean, such as a comparison or a workflow_dispatch input declared type: boolean read through the inputs context. An expression that yields a string is refused as an unexpected value, and since the key is read only after a step has failed, you find that out at the worst possible moment.

Related guides

References

Anything not empty is true, including the word false. Latchkey keeps the real failures visible. Start free → 30-day trial · No credit card