Boolean evaluation - workflow yaml

Are workflows supposed to support booleans, or not? Specific example: suppose a step sets an output using ::set-output name=foo::false, should a if: steps.bar.outputs.foo evaluate that as true or false?

The actions docs suggest false should be evaluated as a boolean:

However my testing shows that not to be the case, example workflow [1] shows that ‘false’ is actually evaluated as true as per this run [2].

Some discussions on this board suggest that this is intentional, false is just treated as a string (which is understandable - if surprising), e.g. here:

But then there’s this example from 2019 (by a Github staffer) that implies that false is actually evaluated as false (unfortunately the logs have expired, but I’m assuming that it worked as intended):
github.community/t/how-to-use-matrix-and-boolean/17034/2
I actually copied that workflow here[3] but it runs both jobs [4].

TL;DR: is treating false as a string REALLY the intended behaviour - or is this just a regression since 2019? If yes: I was thinking about updating the docs to make this explicit, but given that at least one Github staffer is implying that booleans should work I figure it was worth trying to clarify things here first.

Sorry for the link mangling below: I’m a new user and am therefore limited to 2 links per post. Replace GH with github DOT com in the links below:

[1] GH/ahunt/actions-test/commit/552baf4fb28e43a7a8d1450bab81166e331dc448#diff-87ee5504a3e25ac558b343724c905f2f7949e8cec3d92b9c4300bb922afa164f
[2] GH/ahunt/actions-test/actions/runs/718607239
[3] GH/ahunt/actions-test/commit/72bc16e7ec1d0f4f1a07aaffb29f9cd6163ad591
[4] GH/ahunt/actions-test/actions/runs/726067049

Here’s how I understand it:

The expression ${{ false }} evaluates to a boolean, but if you do echo ::set-output name=foo::${{ false }} then you are essentially printing to a console - a text-based system. Everything becomes a string at that point. Hence all outputs are strings. The boolean false is implicitly cast to a string ‘false’.

What you can do to preserve data structures across this boundary is to marshal them as JSON strings and decode them again, like echo ::set-output name=foo::${{ toJSON(false) }} and if: fromJSON(steps.bar.outputs.foo).

I don’t know if automatic decoding was a thing in 2019, but as it works today it seems quite logical and by design that if doesn’t try to interpret output strings.