Worked example: modeling a GitHub Actions workflow¶
A third real, external format — YAML this time, and the first of the four in this set with an unanticipated codec-level finding, not just a schema-level one.
Modeled against SchemaStore's
github-workflow.json,
retrieved 2026-07-11 — community-maintained, not an official GitHub
artifact, same caveat as package.json's schema.
GitHub's own workflow docs are prose, like npm's; no formal spec exists
here either.
Source, code, and fixtures:
examples/github-actions/.
Schema: workflow.osd.
Script: convert.py.
Run it¶
python3 examples/github-actions/convert.py
read_yaml is called with schema= — a no-op here, same as the
pyproject.toml and package.json examples (no date/time/datetime/
number field in this schema either).
A real finding: three of the four fixtures don't even read¶
This repo's own three workflow files
(test.yml,
publish.yml,
docs.yml)
use the ordinary, unquoted on: key — the standard convention almost
every real workflow file uses. All three fail before validation even
starts:
DocumentError: $: object key True is not a string
The cause: PyYAML's safe_load follows YAML 1.1 semantics, where
on/off/yes/no/true/false (in various cases) are boolean
scalars, not strings. A bare on: key parses to the Python boolean
True, not the string "on":
import yaml
yaml.safe_load("on:\n push: {}\n")
# {True: {'push': {}}}
omnist's Document model requires every label to be a string — so
read_yaml correctly refuses to build a Document with a boolean key,
raising DocumentError rather than silently coercing it. This is
correct behavior for read_yaml given what it received; the actual
problem is one level up, in what PyYAML handed it. It's also exactly
the notorious "Norway problem" that YAML 1.1
parsers are known for (NO parsing as false is the classic example;
on is a less-famous but real instance of the same rule) — and it
happens to collide with the one top-level key almost every GitHub
Actions workflow file needs.
test-quoted-on.yml
is test.yml with the key quoted as "on": — a synthetic derivative,
not sourced verbatim, built specifically to demonstrate schema
validation succeeding once past this codec-level issue. Real workflow
files almost never quote this key, which is exactly what makes the
finding worth stating plainly rather than working around it silently.
Its OML is committed as
test-quoted-on.oml,
and its OML arrays form as
test-quoted-on.arrays.oml
— the richest run structure in this whole four-example set: each job's
steps: is a run of 4–5 subtrees mixing uses:- and run:-shaped
entries, collapsed into one array.
This is a limitation of the codec (or arguably of PyYAML's YAML 1.1 defaults), not of OSD's schema algebra — worth keeping separate from the schema-level gaps below.
What OSD can model exactly¶
Only 2 of 8 top-level fields — name, run-name — are fully closed
and checked. That's a quarter, the lowest of all four examples in
this set.
What it can't, and why¶
Unions: on is a three-way union — a bare event string, an
array of event strings, or a map of event → trigger config — one step
past the two-way unions seen in the pyproject.toml and package.json
examples. permissions and concurrency are also unions (a string
shorthand or an object).
Open key sets: jobs (job-id → job config), env, and defaults
are all open maps, same category as [tool]/scripts/dependencies
in the earlier examples.
Shape depends on a sibling key, not just cardinality: inside a
job's steps: array, each item alternates between a uses:-shaped
step (calling a reusable action) and a run:-shaped step (a shell
command) — within the same array. This is the clearest instance in
this whole set of the "meta-referential" pattern flagged in
the overview's lessons list:
the field that's actually present determines what shape the record is,
not a fixed discriminant OSD's cardinality model can check.
The honest number: 6 of 8 fields are any — 75%, again the
highest of the four examples, consistent with jobs alone carrying
most of a workflow's real content and being irreducibly open.
See also: the pyproject.toml example, the package.json example, and the overview for how this compares across all four.