GitHub Actions expression evaluator

Evaluate a GitHub Actions ${{ }} expression against a context JSON and see every type cast the runner applied, including the four casts where the runner disagrees with JavaScript.

Everything runs in this page. Nothing you type is uploaded, logged or sent anywhere. This is an independent tool. It is not affiliated with, endorsed by, or produced by GitHub.

Where the evaluator disagrees with JavaScript

The non-transitive results people quote are a lead-in, not the point. '' == 0, 0 == '0', '' == '0' and 'true' == true return exactly the same answers in raw JavaScript, so knowing JavaScript is enough to predict them. The rows below are the ones JavaScript knowledge gets wrong. String versus string is OrdinalIgnoreCase, so a branch guard on a ref name or a label matches on any casing, and null coerces to 0 before the comparison is retried, so null equals both 0 and the empty string.

Both columns are computed in your browser when this page loads. Nothing is hard coded.
expression this evaluator (Actions semantics) the same operator in JavaScript why they differ
The lead-in rows. Identical on both sides, which is why they are not the headline.
expression this evaluator JavaScript

Evaluate an expression

Verdict

Trace

One row per node. For a comparison the row names the kind of each operand, the CoerceTypes branch that fired, and the branch that produced the verdict.

# node left right cast applied branch that produced the verdict result

Self test

The strip below runs on load. The full suite runs on the button. It contains one positive control, an assertion written to fail on purpose, so a suite that silently stops running is visible rather than green.

What this evaluator does not do

Provenance

The coercion, comparison, truthiness and number parsing behaviour was reimplemented in JavaScript from GitHub's published runner sources, fetched and read on 2026-09-09. No code is reproduced here. The files read, with the byte sizes they had that day:

The rules were cross-checked against GitHub's own published port, the npm package @actions/expressions version 0.3.61 (MIT, repository actions/languageservices), read on the same day.

The comment sitting above the equality functions in EvaluationResult.cs, lines 78 and 79, reads:

Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.

The published documentation cast table is incomplete as a description of ==. It lists Array and Object casting to NaN, but CoerceTypes has no Array or Object branch on either side, so a container compared with == never reaches a NaN cast. It falls through to the kind check and returns false, and two containers of the same kind are compared by reference. The NaN rows are unreachable from every comparison operator, equality and relational alike.

The cast table's String row begins "Parsed from any legal JSON number format, otherwise NaN". The runner's parser is wider than that. It accepts hexadecimal, octal, a leading plus, surrounding whitespace and the literal Infinity in any casing, and not one of those is a legal JSON number. The casing was measured on a hosted runner on 2026-09-21, job 35656787367. It also rejects binary literals and the uppercase 0X and 0O prefixes that JavaScript's Number() accepts, which is the table below.

Known divergence from GitHub's own JavaScript port

GitHub also publishes a JavaScript port of this language, the npm package @actions/expressions, built in the repository actions/languageservices and MIT licensed. In version 0.3.61 of that package, the string to number cast is a plain JavaScript Number() call, in dist/data/string.js at line 12, so it disagrees with the C# runner on '0b101', '0X10', '0O17', '0xFFFFFFFF' and '0x100000000'. This page follows the C# runner, because that is the implementation that executes on hosted runners. All five are self test rows.

What was measured on a hosted runner, and what was not

Sixteen expressions were run in a workflow on a GitHub hosted ubuntu-latest runner on 2026-09-21 and their output read back verbatim, in jobs 35656695534 and 35656787367. This evaluator returns the same verdict on all sixteen. They are:

Measured on a GitHub hosted ubuntu-latest runner, 2026-09-21. The runner column is the raw job output, copied verbatim.
expression runner this evaluator job
'' == 0truetrue35656695534
0 == '0'truetrue35656695534
'' == '0'falsefalse35656695534
'true' == truefalsefalse35656695534
'ABC' == 'abc'truetrue35656695534
null == 0truetrue35656695534
fromJSON('[]') == 0falsefalse35656695534
'infinity' == 0falsefalse35656695534
'0x10' == 16truetrue35656695534
'0o17' == 15truetrue35656695534
'infinity' > 0truetrue35656787367
'Infinity' > 0truetrue35656787367
'INFINITY' > 0truetrue35656787367
'NaN' > 0falsefalse35656787367
'+5' == 5truetrue35656787367
' 5 ' == 5truetrue35656787367

The second job exists because equality cannot tell Infinity apart from NaN: 'infinity' == 0 and 'NaN' == 0 are both false, whichever one the string parsed to. A relational comparison separates them, and it settled the open question. Lowercase 'infinity' does parse to a number on the runner, and the literal is matched case insensitively, so this page now parses 'infinity', 'Infinity' and 'INFINITY' alike. 'NaN' is not a parse branch.

Every other result on this page comes from this evaluator, from raw JavaScript running in your browser, or from the runner source files listed above, and was not read off a hosted runner. That includes the whole G15 number format table, the hexadecimal and octal Int32 edge cases beyond the two rows above, the OrdinalIgnoreCase folding rows, the truthiness rows and every context lookup row. A hosted runner measurement is also narrower than it sounds: it is one runner image on one date, it reports what that runner version did, and it is not a promise about a future runner. Where a row matters to you, run it in a workflow and check.