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

> GitHub Actions unable to interpret as boolean appears in no parser GitHub ships. See where a boolean key raises it, and the case that stays silent.

Source: https://latchkey.dev/learn/github-actions/github-actions-unable-to-interpret-as-boolean  
Updated: 2026-09-20

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.

## 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 ''
```

## 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' }}
```

## 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.

## 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.

## 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 wrote | How the parser reads it | What happens |
| --- | --- | --- |
| `continue-on-error: true` | a boolean | works |
| `continue-on-error: yes` | the 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: boolean` | a boolean | works |
| `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.

## FAQ

### 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.

## References

- [GitHub Actions: workflow syntax, on.workflow_dispatch.inputs and the Boolean note](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax)
- [actions/runner#2418: the unexpected-value error, captured mid-run](https://github.com/actions/runner/issues/2418)
- [actions/runner#1483: boolean inputs are not actually booleans](https://github.com/actions/runner/issues/1483)
- [github/vscode-github-actions#33: the BooleanToken message, quoted](https://github.com/github/vscode-github-actions/issues/33)
- [actions/runner: YamlObjectReader.cs, the six spellings that are booleans](https://github.com/actions/runner/blob/main/src/Sdk/WorkflowParser/Conversion/YamlObjectReader.cs)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
