How to convert github actions workflow yaml to json
- Step 1Open the workflow file — Copy the contents of
.github/workflows/ci.yml(or drop the.ymlfile). Matrixstrategyblocks, reusable-workflowuses:calls, andenvmaps all come through as-is. - Step 2Leave multi-doc off for a normal workflow — A workflow file is a single YAML document, so keep Multi-document YAML (---) off. Only turn it on if you have concatenated several workflows into one file with
---separators (uncommon). - Step 3Pick an indent — Minified for a payload you will feed to another tool; 2 or 4 spaces for a readable tree you will eyeball or commit next to the YAML.
- Step 4Convert to JSON — Click Convert to JSON. Confirm the top-level keys:
name,on,env, andjobs. Noteonappears as a quoted string key in the JSON object. - Step 5Query the structure — Pull the fields you need with the JSON Path Extractor — for example
$.jobs.*.steps[*].usesfor every action used, or$.jobs.*.steps[*].runfor every shell script across the workflow. - Step 6Feed it to your tooling — Pass the JSON to a security scanner, a workflow visualizer, or a test that checks every job runs a required step. Because expressions stay literal, pattern-matching for
secrets.or${{works directly on the strings.
What survives conversion (CI-specific)
How the parts of a workflow that matter to CI tooling appear in the JSON output.
| Workflow element | JSON result | Notes |
|---|---|---|
${{ secrets.TOKEN }} | literal string "${{ secrets.TOKEN }}" | Never evaluated — greppable for audits |
on: trigger key | string key "on" | YAML 1.2 core schema keeps it a string, not a boolean |
run: | (literal block) | string with \n line breaks | Every line of the script is preserved |
run: > (folded block) | string with newlines folded to spaces | Per the YAML spec for folded scalars |
needs: [a, b] | JSON array ["a","b"] | Dependency edges intact |
with: inputs map | JSON object | Action input names and values preserved |
Converter controls
The two UI controls. There is no expression evaluation, no schema check — pure YAML-to-JSON.
| Control | Values | Effect |
|---|---|---|
| Multi-document YAML (---) | on / off | Keep off for a single workflow. On (with ---) yields a JSON array of documents |
| Indent | Minified · 2 · 4 spaces | Output formatting only; values unchanged |
| Free tier limit | 2 MB, 1 file | Workflow files are tiny, so this is never a constraint in practice |
Cookbook
Workflow snippets and the exact JSON the converter emits. The recurring point: expressions stay literal, and on becomes a string key.
A minimal CI workflow to JSON
ExampleTop-level name, on, and jobs map straight across. Note that on is a quoted string key in the output.
Input (ci.yml):
name: CI
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Output JSON (2-space):
{
"name": "CI",
"on": { "push": { "branches": ["main"] } },
"jobs": { "build": {
"runs-on": "ubuntu-latest",
"steps": [ { "uses": "actions/checkout@v4" } ]
} }
}Secret expressions preserved for an audit
ExampleThe converter never resolves ${{ }}. The literal string survives so you can scan the JSON for every secret a workflow touches.
Input:
jobs:
deploy:
steps:
- run: deploy --token ${{ secrets.DEPLOY_TOKEN }}
env:
API_KEY: ${{ secrets.API_KEY }}
Output JSON (expressions literal):
{ "jobs": { "deploy": { "steps": [ {
"run": "deploy --token ${{ secrets.DEPLOY_TOKEN }}",
"env": { "API_KEY": "${{ secrets.API_KEY }}" }
} ] } } }
Audit: grep the JSON for `secrets.` → both refs found.Multi-line `run: |` script keeps every line
ExampleA literal block scalar becomes a JSON string with \n between lines, so a scanner sees the whole script.
Input:
steps:
- run: |
npm ci
npm run build
npm test
Output JSON:
{ "steps": [ { "run": "npm ci\nnpm run build\nnpm test\n" } ] }
(Each command on its own line, newlines preserved.)Extract every action used across all jobs
ExampleConvert, then run a JSONPath over the result to list dependencies for a pinning audit.
After converting workflow.yml to JSON, in the JSON Path Extractor: $.jobs.*.steps[*].uses Result: [ "actions/checkout@v4", "actions/setup-node@v4", "docker/build-push-action@v5" ]
Folded `>` description collapses newlines
ExampleFolded block scalars behave per spec — newlines become spaces. Use | instead if you need the line breaks kept.
Input:
jobs:
build:
name: >
Build and
test the app
Output JSON:
{ "jobs": { "build": { "name": "Build and test the app\n" } } }
(The two lines folded into one space-joined string.)Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
`${{ }}` expression you expected to be resolved
By designExpressions are preserved as literal strings, never evaluated — there is no GitHub context to evaluate against in a converter. This is the correct behavior for auditing (you can grep for secrets.), but if you wanted the resolved value, that only exists at runtime on GitHub's runners.
`on:` key in the output is a string, not a boolean
ExpectedSome older YAML 1.1 tools coerce the bare word on to boolean true, mangling the trigger block. js-yaml 4.x uses the YAML 1.2 core schema, so on stays the string key "on" and $.on.push resolves correctly. No quoting needed.
Folded `>` script collapsed your shell newlines
Spec behaviorA folded block scalar (>) joins lines with spaces, which breaks a multi-command shell script. Workflows should use the literal block scalar (|) for run: so newlines are kept. If your output script is one space-joined line, the source used > — switch it to |.
Tab used to indent a step
Tab errorYAML forbids tab indentation; a tab throws tab characters must not be used in indentation with line and column. GitHub's own parser would reject the workflow too. Re-indent with spaces.
Duplicate job key or duplicate step key
Duplicate key errorTwo jobs with the same id, or any repeated key in one mapping, throw duplicated mapping key. The parser rejects rather than silently keeping the last one — fix the workflow so ids are unique.
Concatenated workflows with `---` and toggle off
Parse errorIf you pasted several workflows into one file separated by --- and left the multi-doc toggle off, you get expected a single document in the stream, but found more. Turn on Multi-document YAML (---) to get a JSON array of workflows.
Unknown custom tag in a value
Unknown tag errorStandard workflows have no custom tags, but an accidental !-prefixed tag throws unknown tag. The parser only knows YAML 1.2 standard tags. Remove the tag or quote the value.
Empty workflow file
Undefined outputAn empty file or comments-only file parses to undefined, and the output is the literal undefined rather than {}. Paste the actual workflow content.
Anchors used for a shared step block
Preserved (expanded)Some teams DRY up matrix steps with YAML anchors. The converter resolves them — the JSON has the anchored block expanded inline at every alias site, with no &/* left. The output is functionally identical to GitHub's expansion.
Frequently asked questions
Why is `on` a string key instead of `true` in my JSON?
That is correct behavior. js-yaml 4.x uses the YAML 1.2 core schema, which does not coerce the word on to a boolean. So your on: trigger block becomes the string key "on" and $.on.push.branches works. Older YAML 1.1 tools that turn on into true are the buggy ones.
Are `${{ secrets.X }}` expressions resolved?
No, and that is intentional. Expressions are preserved as literal strings because there is no GitHub runtime context to evaluate against. This makes the JSON ideal for security audits — you can grep it for secrets. to find every secret the workflow references.
How do I list every action a workflow uses?
Convert the workflow to JSON, then run $.jobs.*.steps[*].uses in the JSON Path Extractor. You get a flat array of every uses: value across all jobs — perfect for an action-pinning or supply-chain audit.
Will multi-line `run:` scripts be preserved?
Yes, if they use the literal block scalar |. Each line is kept and joined with \n in the JSON string. Folded scalars (>) collapse newlines to spaces per the YAML spec, so use | for shell scripts where line breaks matter.
Can it validate the workflow against GitHub's schema?
No — it only converts YAML to JSON. It does not check that on, jobs, or runs-on are valid. After converting, you can structurally check the JSON with the JSON Validator, but GitHub's own workflow linter is the authority on workflow validity.
Do job `needs:` dependencies survive?
Yes. A needs: [build, lint] becomes a JSON array ["build","lint"], so you can reconstruct the dependency graph from $.jobs.*.needs and assert startup order or detect cycles in your own tooling.
Are my secret names uploaded anywhere?
No. Parsing runs entirely in your browser with js-yaml. Secret names, environment names, runner labels, and every other value stay on your machine. Nothing is transmitted to JAD Apps servers.
Why did my workflow throw `expected a single document in the stream`?
You have more than one YAML document (a --- separator) and the multi-doc toggle is off. Single workflows do not need it, but if you concatenated several files, turn on Multi-document YAML (---) to get an array of workflows.
Can I convert the JSON back to a workflow YAML?
Yes, with the inverse JSON to YAML tool. If you generate or template a workflow as JSON, convert it to YAML to commit it under .github/workflows/.
Does it preserve the matrix strategy block?
Yes. The entire strategy.matrix block is parsed as nested JSON objects and arrays exactly as written. You can query $.jobs.*.strategy.matrix to enumerate matrix dimensions for documentation or test generation.
Can I diff two workflow versions as JSON?
Yes. Convert both workflow versions to JSON, then compare them with the JSON Diff tool to see exactly which steps, inputs, or triggers changed between two revisions — clearer than a raw YAML diff that is sensitive to formatting.
Is there an API so I can do this in CI?
Pro unlocks API access for the JSON tools, so you can convert workflows to JSON programmatically as part of a pipeline rather than pasting into the browser. A typical use is converting then asserting required steps exist with a JSONPath check.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.