GitHub Actions JSON: fromJSON, toJSON, Dynamic Matrices & Outputs
Last updated:
GitHub Actions provides two JSON functions in workflow expressions: fromJSON(value) converts a JSON string to a value (use in strategy.matrix for dynamic matrices), and toJSON(value) serializes a value to a formatted JSON string (use to debug context objects). Setting a JSON output from a step: echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT, then reading it with fromJSON(needs.build.outputs.matrix) in a downstream job's strategy.matrix. GitHub Actions context objects (github, runner, env) are all JSON-serializable — ${{ toJSON(github) }} dumps the full context as a formatted JSON string for debugging. This guide covers fromJSON/toJSON expression functions, dynamic job matrices from JSON, step outputs as JSON strings, parsing curl JSON responses in shell steps, workflow_dispatch JSON inputs, and the actions/github-script action for complex JSON manipulation.
fromJSON() and toJSON(): Expression Functions
GitHub Actions expression functions fromJSON() and toJSON() bridge the gap between shell string values and typed workflow data. fromJSON(value) parses a JSON string and returns the corresponding value — object, array, boolean, number, or null — usable anywhere an expression value is accepted. toJSON(value) does the reverse: it takes any expression value (context object, string, boolean) and returns a pretty-printed JSON string. Both functions are available in all expression contexts: if: conditions, strategy.matrix, step env:, job outputs:, and run: inline expressions.
# fromJSON() — parse a JSON string to a typed value
# Use case 1: boolean from string
- name: Set flag
id: flags
run: echo "skip=true" >> $GITHUB_OUTPUT
- name: Conditional step
if: ${{ fromJSON(steps.flags.outputs.skip) == true }}
run: echo "Skipping..."
# Use case 2: number comparison
- name: Check score
run: echo "score=95" >> $GITHUB_OUTPUT
- name: Pass threshold
if: ${{ fromJSON(steps.check.outputs.score) >= 80 }}
run: echo "Score passed"
# Use case 3: array from JSON string
- name: Build matrix list
id: list
run: |
echo 'items=["a","b","c"]' >> $GITHUB_OUTPUT
- name: Use as array
run: |
# In expressions, fromJSON converts the string to an array
echo "Length: ${{ join(fromJSON(steps.list.outputs.items), ', ') }}"
# toJSON() — serialize any value to a JSON string
# Use case 1: dump the full github context for debugging
- name: Debug github context
run: echo '${{ toJSON(github) }}'
# Use case 2: dump event payload (the webhook body that triggered the run)
- name: Print event payload
run: |
cat <<'EOF'
${{ toJSON(github.event) }}
EOF
# Use case 3: dump all step results
- name: Debug steps
if: always()
run: echo '${{ toJSON(steps) }}'
# Use case 4: dump needs (upstream job outputs and results)
- name: Debug upstream jobs
run: echo '${{ toJSON(needs) }}'
# Use case 5: toJSON in an env variable
- name: Pass context to script
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: |
echo "$GITHUB_CONTEXT" | jq .repositoryfromJSON() throws an expression evaluation error if the input string is not valid JSON — the workflow step fails immediately, which is desirable behavior (fail fast rather than silently treating malformed JSON as a string). When generating JSON for fromJSON() consumption, always validate with jq empty <<< "$JSON" in the generating step before writing to GITHUB_OUTPUT. toJSON() is safe on any value and never fails — it produces null for undefined values and handles circular references in context objects gracefully.
Dynamic Job Matrices with fromJSON()
Static matrices hardcode build targets in the workflow YAML — dynamic matrices compute targets at runtime from a prior job output, an API response, or a file in the repository. The pattern requires three components: a setup job that generates JSON and writes it to GITHUB_OUTPUT, a job-level outputs: declaration that exposes the step output, and a matrix job that uses fromJSON() to expand the JSON string into matrix dimensions. The matrix job must declare needs: [setup-job] to access the outputs.
jobs:
# Step 1: generate the matrix JSON
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.gen.outputs.matrix }}
steps:
- uses: actions/checkout@v4
# Option A: generate from a file in the repo
- name: Load matrix from file
id: gen
run: |
# matrix.json: [{"node":"18","os":"ubuntu"},{"node":"20","os":"macos"}]
echo "matrix=$(jq -c . .github/matrix.json)" >> $GITHUB_OUTPUT
# Option B: generate dynamically from an API call
- name: Generate from API
id: gen-api
run: |
ENVS=$(curl -s https://api.example.com/active-environments | jq -c '[.[] | {env: .name, region: .region}]')
echo "matrix=$ENVS" >> $GITHUB_OUTPUT
# Option C: build inline with jq
- name: Build inline matrix
id: gen-inline
run: |
MATRIX=$(jq -cn '
[
{"node": "18", "os": "ubuntu-latest"},
{"node": "20", "os": "ubuntu-latest"},
{"node": "22", "os": "ubuntu-latest"},
{"node": "20", "os": "macos-latest"}
]
')
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
# Step 2: consume the matrix in parallel jobs
build:
needs: setup
runs-on: ${{ matrix.os }}
strategy:
matrix:
include: ${{ fromJSON(needs.setup.outputs.matrix) }}
fail-fast: false
max-parallel: 4
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
# Advanced: combine fromJSON matrix with static dimensions
test-cross:
needs: setup
runs-on: ubuntu-latest
strategy:
matrix:
service: ${{ fromJSON(needs.setup.outputs.matrix) }}
browser: [chrome, firefox]
# Exclude combinations
exclude:
- browser: firefox
service: {name: "legacy-service"}
steps:
- run: echo "Testing ${{ matrix.service.name }} on ${{ matrix.browser }}"The JSON written to GITHUB_OUTPUT must be compact (single-line) — multi-line JSON breaks the KEY=VALUE format that Actions uses to parse the output file. Always use jq -c (compact output) when generating matrix JSON. If your JSON source is already a file, read it with jq -c . to strip whitespace. The include key in strategy.matrix is the most flexible entry point for fromJSON() because it accepts an array of objects, where each object adds or overrides matrix dimensions for a specific combination. For multi-environment deployments targeting Kubernetes JSON configs or Docker JSON config, dynamic matrices are the standard approach to fan out identical jobs across cluster targets.
Setting JSON Step Outputs with GITHUB_OUTPUT
Step outputs in GitHub Actions are string key-value pairs written to the GITHUB_OUTPUT environment file. Because outputs are strings, JSON values must be serialized before writing and deserialized after reading. The critical constraint: the value portion cannot contain unescaped newlines in the simple KEY=VALUE format — always use jq -c to produce compact JSON or the heredoc delimiter syntax for multi-line values.
jobs:
producer:
runs-on: ubuntu-latest
outputs:
# Expose step outputs at the job level for downstream jobs
result: ${{ steps.process.outputs.result }}
config: ${{ steps.process.outputs.config }}
steps:
- uses: actions/checkout@v4
# Pattern 1: compact JSON object as output
- name: Set JSON output (compact)
id: process
run: |
RESULT=$(jq -cn '{
"status": "success",
"version": "1.2.3",
"count": 42,
"tags": ["release", "stable"]
}')
echo "result=$RESULT" >> $GITHUB_OUTPUT
# Pattern 2: compact JSON from file
- name: Set JSON from file
id: from-file
run: |
echo "config=$(jq -c . config.json)" >> $GITHUB_OUTPUT
# Pattern 3: multi-line JSON using heredoc delimiter syntax
# Use when the JSON might contain = signs or other special characters
- name: Set multi-line JSON output
id: multiline
run: |
JSON=$(cat <<'ENDJSON'
{
"key": "value with = signs",
"nested": {"a": 1}
}
ENDJSON
)
echo "data<<EOF" >> $GITHUB_OUTPUT
echo "$(echo "$JSON" | jq -c .)" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Pattern 4: build JSON incrementally
- name: Build output JSON
id: build
run: |
VERSION=$(git describe --tags --abbrev=0)
SHA=$(git rev-parse --short HEAD)
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
JSON=$(jq -cn --arg version "$VERSION" --arg sha "$SHA" --arg ts "$TIMESTAMP" '{version: $version, sha: $sha, timestamp: $ts}')
echo "meta=$JSON" >> $GITHUB_OUTPUT
consumer:
needs: producer
runs-on: ubuntu-latest
steps:
# Read JSON output from same job (steps.id.outputs.key)
# — only in same job, different steps
# Read job output from upstream job (needs.job.outputs.key)
- name: Use upstream JSON output
env:
RESULT_JSON: ${{ needs.producer.outputs.result }}
run: |
echo "Status: $(echo "$RESULT_JSON" | jq -r .status)"
echo "Version: $(echo "$RESULT_JSON" | jq -r .version)"
echo "Count: $(echo "$RESULT_JSON" | jq -r .count)"
# Use fromJSON() in expression to access fields directly
- name: Conditional on JSON field
if: ${{ fromJSON(needs.producer.outputs.result).status == 'success' }}
run: echo "Build was successful"The old ::set-output name=key::value workflow command syntax was deprecated in September 2022 and disabled in June 2023 — always use the GITHUB_OUTPUT file approach. When a JSON value contains the literal string EOF (rare but possible), choose a different heredoc delimiter. The 1 MB size limit applies per output value — for deployment manifests, test result files, or other large JSON, write to a file and upload as an artifact using actions/upload-artifact, then download in subsequent jobs with actions/download-artifact.
Parsing curl JSON Responses in Shell Steps
GitHub Actions shell steps run in a standard Linux/macOS/Windows environment with curl and jq pre-installed on all runner types. The canonical pattern for consuming a JSON API in a workflow step: fetch with curl -s, pipe to jq, and write selected fields to GITHUB_OUTPUT for use in later steps or jobs. Always handle HTTP errors explicitly — curl exits 0 even on 4xx/5xx responses by default.
- name: Call JSON API and extract fields
id: api
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
run: |
# -s: silent (no progress bar)
# -f: fail on 4xx/5xx (exit code 22)
# -H: set headers
RESPONSE=$(curl -sf -H "Authorization: Bearer $API_TOKEN" -H "Accept: application/json" https://api.example.com/releases/latest)
# Exit if curl failed
if [ $? -ne 0 ]; then
echo "API call failed" >&2
exit 1
fi
# Extract fields with jq
VERSION=$(echo "$RESPONSE" | jq -r .tag_name)
DOWNLOAD_URL=$(echo "$RESPONSE" | jq -r '.assets[0].browser_download_url')
PRERELEASE=$(echo "$RESPONSE" | jq -r .prerelease)
# Write to GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "download_url=$DOWNLOAD_URL" >> $GITHUB_OUTPUT
echo "prerelease=$PRERELEASE" >> $GITHUB_OUTPUT
# Write full response as compact JSON output
echo "payload=$(echo "$RESPONSE" | jq -c .)" >> $GITHUB_OUTPUT
- name: Use GitHub REST API with GITHUB_TOKEN
id: gh-api
run: |
# GitHub token is available automatically — no secrets needed
RESPONSE=$(curl -sf -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/repos/${{ github.repository }}/pulls?state=open")
PR_COUNT=$(echo "$RESPONSE" | jq 'length')
echo "open_prs=$PR_COUNT" >> $GITHUB_OUTPUT
- name: POST JSON to an API
run: |
PAYLOAD=$(jq -cn --arg ref "${{ github.sha }}" --arg env "staging" '{ref: $ref, environment: $env, auto_merge: false}')
curl -sf -X POST -H "Content-Type: application/json" -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" -d "$PAYLOAD" https://api.example.com/deployments
- name: Handle paginated JSON responses
run: |
PAGE=1
ALL_ITEMS="[]"
while true; do
RESPONSE=$(curl -sf -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/${{ github.repository }}/issues?per_page=100&page=$PAGE")
COUNT=$(echo "$RESPONSE" | jq 'length')
if [ "$COUNT" -eq 0 ]; then break; fi
ALL_ITEMS=$(echo "$ALL_ITEMS $RESPONSE" | jq -sc 'add')
PAGE=$((PAGE + 1))
done
echo "total=$(echo "$ALL_ITEMS" | jq 'length')" >> $GITHUB_OUTPUTUse -sf (silent + fail) rather than -s alone so that curl exits non-zero on HTTP errors, causing the workflow step to fail visibly rather than silently producing an empty or error-body response. When the API response contains HTML error pages (common with misconfigured proxies or CDNs), jq will fail with a parse error — add -H "Accept: application/json" to request JSON specifically. For GitHub API calls, always include the X-GitHub-Api-Version: 2022-11-28 header to pin to a stable API version and avoid unexpected breaking changes. See also: AWS JSON APIs for patterns when calling AWS service endpoints from workflow steps.
workflow_dispatch JSON Inputs
workflow_dispatch triggers allow manual workflow runs with user-supplied inputs. GitHub Actions does not have a native JSON input type — all workflow_dispatch inputs are strings regardless of the type: declaration. To accept JSON, define a string input, document the expected JSON schema in the description, provide a valid JSON default, and parse with fromJSON() or jq inside the workflow. Input values from workflow_dispatch are available as ${ ${{ inputs.name }} } in expressions and ${ ${{ inputs.name }} } in env: blocks for shell access.
on:
workflow_dispatch:
inputs:
# JSON string input — document the schema in description
config:
description: |
JSON deployment config. Schema:
{"env": "staging|production", "replicas": 1-10, "debug": true|false}
required: true
type: string
default: '{"env":"staging","replicas":2,"debug":false}'
# Simple JSON array input
services:
description: 'JSON array of service names, e.g. ["api","worker","scheduler"]'
required: false
type: string
default: '["api","worker"]'
# JSON object for feature flags
features:
description: 'Feature flags as JSON object'
required: false
type: string
default: '{"new_ui":false,"beta_api":false}'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# Validate JSON inputs before using them
- name: Validate inputs
run: |
echo '${{ inputs.config }}' | jq empty || {
echo "Invalid JSON in config input" >&2
exit 1
}
echo '${{ inputs.services }}' | jq empty || {
echo "Invalid JSON in services input" >&2
exit 1
}
# Access fields with fromJSON() in expressions
- name: Deploy to environment
env:
TARGET_ENV: ${{ fromJSON(inputs.config).env }}
REPLICAS: ${{ fromJSON(inputs.config).replicas }}
run: |
echo "Deploying to $TARGET_ENV with $REPLICAS replicas"
# Access fields in shell with jq
- name: Parse config in shell
run: |
CONFIG='${{ inputs.config }}'
ENV=$(echo "$CONFIG" | jq -r .env)
REPLICAS=$(echo "$CONFIG" | jq -r .replicas)
DEBUG=$(echo "$CONFIG" | jq -r .debug)
echo "Environment: $ENV, Replicas: $REPLICAS, Debug: $DEBUG"
# Use JSON array input as a matrix in a child workflow
# (or iterate in a shell loop)
- name: Process each service
run: |
SERVICES='${{ inputs.services }}'
echo "$SERVICES" | jq -r '.[]' | while read -r SERVICE; do
echo "Processing service: $SERVICE"
done
# Conditional logic based on JSON field
- name: Enable debug mode
if: ${{ fromJSON(inputs.config).debug == true }}
run: echo "Debug mode is enabled"
# Merge inputs with defaults using jq
- name: Apply defaults to sparse input
run: |
DEFAULTS='{"env":"staging","replicas":1,"debug":false,"timeout":300}'
INPUT='${{ inputs.config }}'
MERGED=$(echo "$DEFAULTS $INPUT" | jq -sc 'add')
echo "Final config: $MERGED"When invoking a workflow_dispatch workflow via the GitHub API or gh workflow run, pass JSON inputs as strings: gh workflow run deploy.yml -f config='{"env":"production","replicas":5}'. Single-quote the JSON string in shell to prevent the shell from interpreting the braces. In GitHub's web UI, the input field shows a text box — users must type valid JSON manually, so provide a clear description and a valid default. For complex configuration, consider a YAML or JSON config file committed to the repository and a simpler string input (like an environment name) that selects which config to load, reducing the risk of malformed JSON from manual input.
actions/github-script for Complex JSON Operations
The actions/github-script action runs JavaScript directly in a workflow step with full access to the GitHub API via the authenticated github Octokit client and the core toolkit for setting outputs and logging. It is the right tool when JSON manipulation requires logic too complex for shell and jq — nested transformations, conditional API calls, or building structured outputs from multiple API responses.
- name: Get release data and build deployment matrix
id: matrix
uses: actions/github-script@v7
with:
script: |
// Fetch releases from GitHub API (authenticated automatically)
const { data: releases } = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 5,
});
// Build a matrix array from release data
const matrix = releases
.filter(r => !r.prerelease && !r.draft)
.map(r => ({
version: r.tag_name,
download_url: r.assets[0]?.browser_download_url ?? '',
published_at: r.published_at,
}));
// Set the matrix as a JSON string output
core.setOutput('matrix', JSON.stringify(matrix));
// Log for debugging (visible in the step log)
core.info(`Generated matrix with ${matrix.length} entries`);
core.info(JSON.stringify(matrix, null, 2));
- name: Parse and transform JSON payload
uses: actions/github-script@v7
with:
script: |
// Access the event payload (already parsed as an object)
const payload = context.payload;
const prNumber = payload.pull_request?.number;
if (!prNumber) {
core.setFailed('No pull request number in event payload');
return;
}
// Fetch PR reviews as JSON
const { data: reviews } = await github.rest.pulls.listReviews({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
// Aggregate review states
const summary = reviews.reduce((acc, review) => {
acc[review.state] = (acc[review.state] ?? 0) + 1;
return acc;
}, {});
core.setOutput('review_summary', JSON.stringify(summary));
core.setOutput('approved', String(summary.APPROVED >= 2));
- name: Set multiple JSON outputs
uses: actions/github-script@v7
with:
script: |
// Read a JSON file from the workspace
const fs = require('fs');
const config = JSON.parse(fs.readFileSync('deployment-config.json', 'utf8'));
// Validate required fields
const required = ['environment', 'region', 'replicas'];
for (const field of required) {
if (config[field] === undefined) {
core.setFailed(`Missing required field: ${field}`);
return;
}
}
// Set individual outputs
core.setOutput('environment', config.environment);
core.setOutput('region', config.region);
core.setOutput('replicas', String(config.replicas));
// Set the entire config as a JSON string output
core.setOutput('config', JSON.stringify(config));
// Set output conditionally
if (config.environment === 'production') {
core.setOutput('requires_approval', 'true');
}actions/github-script uses the same Octokit client as the rest of the GitHub Actions infrastructure — the GITHUB_TOKEN is injected automatically, and all API calls are pre-authenticated. The context object mirrors the workflow expression github context, but as a parsed JavaScript object rather than a string. Use core.setOutput(key, value) to set step outputs — values must be strings, so serialize objects with JSON.stringify(). For very large JSON payloads (approaching 1 MB), write to a file with fs.writeFileSync() and upload as an artifact rather than setting as a step output. Following JSON best practices for naming, structure, and validation applies equally to JSON passing between GitHub Actions jobs and to JSON consumed by external APIs.
Debugging Workflows: Dumping Context JSON with toJSON()
GitHub Actions context objects — github, env, vars, job, steps, runner, secrets, strategy, matrix, needs, and inputs — contain all the runtime information available to a workflow. toJSON() is the fastest way to inspect these objects without setting up external logging. Add a dedicated debug step that always runs (if: always()) to capture context state even when prior steps fail.
jobs:
debug-all-contexts:
runs-on: ubuntu-latest
steps:
- name: Dump all contexts
if: always()
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
ENV_CONTEXT: ${{ toJSON(env) }}
VARS_CONTEXT: ${{ toJSON(vars) }}
JOB_CONTEXT: ${{ toJSON(job) }}
RUNNER_CONTEXT: ${{ toJSON(runner) }}
STEPS_CONTEXT: ${{ toJSON(steps) }}
INPUTS_CONTEXT: ${{ toJSON(inputs) }}
MATRIX_CONTEXT: ${{ toJSON(matrix) }}
run: |
echo "=== GITHUB CONTEXT ==="
echo "$GITHUB_CONTEXT" | jq .
echo "=== EVENT PAYLOAD ==="
echo "$GITHUB_CONTEXT" | jq .event
echo "=== RUNNER CONTEXT ==="
echo "$RUNNER_CONTEXT" | jq .
echo "=== JOB CONTEXT ==="
echo "$JOB_CONTEXT" | jq .
# Most useful: dump just the event payload
- name: Print event payload
run: |
cat <<'EOF'
${{ toJSON(github.event) }}
EOF
# Pretty-print with jq for better readability
- name: Debug github context (pretty)
run: |
echo '${{ toJSON(github) }}' | jq '{
event_name: .event_name,
ref: .ref,
sha: .sha,
actor: .actor,
workflow: .workflow,
run_id: .run_id
}'
# Dump needs context in a downstream job
downstream:
needs: [build, test]
runs-on: ubuntu-latest
if: always()
steps:
- name: Inspect upstream job results
run: |
echo '${{ toJSON(needs) }}' | jq '{
build_result: .build.result,
test_result: .test.result,
build_outputs: .build.outputs,
test_outputs: .test.outputs
}'
# Conditional logic based on toJSON + fromJSON round-trip
- name: Check if all jobs succeeded
run: |
NEEDS='${{ toJSON(needs) }}'
FAILED=$(echo "$NEEDS" | jq '[.[] | select(.result != "success")] | length')
if [ "$FAILED" -gt 0 ]; then
echo "Some upstream jobs failed:"
echo "$NEEDS" | jq '.[] | select(.result != "success") | {result}'
exit 1
fiThe secrets context is intentionally excluded from toJSON(secrets) output — GitHub redacts secret values and replaces them with *** in logs and context dumps. toJSON(github.event) is the most valuable debugging tool when diagnosing webhook-triggered workflow behavior — it shows the exact payload sent by GitHub, including all branch, PR, commit, and pusher details. Enable the ACTIONS_STEP_DEBUG secret (set to true) to automatically enable verbose debug logging, which outputs additional toJSON-equivalent context information for every step without modifying the workflow file.
Key Terms
- fromJSON()
- A GitHub Actions expression function that parses a JSON string and returns the corresponding typed value — object, array, string, number, boolean, or null. Used anywhere an expression is valid:
strategy.matrixto expand a JSON string into matrix dimensions,if:conditions to access fields of JSON outputs, andenv:to pass structured data to shell steps. The input must be a valid JSON string; malformed input causes an expression evaluation error that fails the step immediately. Available in all GitHub Actions expression contexts since the introduction of the expression engine. - toJSON()
- A GitHub Actions expression function that serializes any value to a pretty-printed JSON string. Accepts any expression value: context objects (
github,runner,job,steps,needs,matrix,inputs), booleans, numbers, strings, arrays, and objects. Never fails — returnsnullfor undefined values and handles all built-in context types. Primary use case: debugging workflows by dumping context objects to the step log. In stepenv:blocks, the serialized string is available as an environment variable that can be piped tojqfor further processing. - GITHUB_OUTPUT
- An environment variable that holds the path to a special file used for passing outputs between steps and jobs. Write outputs using
echo "key=value" >> $GITHUB_OUTPUT. Values must be strings; JSON values must be compact (no newlines) in the simple format. Multi-line values use the heredoc delimiter syntax:echo "key<<EOF", the value, thenecho "EOF". Step outputs are read within the same job with${ ${{ steps.step-id.outputs.key }} }and in downstream jobs (via the job-leveloutputs:declaration) with${ ${{ needs.job-id.outputs.key }} }. Maximum value size: 1 MB. Replaced the deprecated::set-outputworkflow command in 2022. - strategy.matrix
- A GitHub Actions job property that creates parallel job runs by expanding a set of dimensions into all combinations (or explicit
includeentries). Defined as a YAML map of dimension names to arrays:matrix: node: [18, 20, 22]creates three parallel jobs. Theincludekey adds extra combinations or properties to existing ones;excluderemoves specific combinations. WithfromJSON(), matrix values can be computed at runtime from a prior job output:include: ${ ${{ fromJSON(needs.setup.outputs.matrix) }} }.fail-fast(default true) cancels remaining matrix jobs when any job fails;max-parallellimits concurrent jobs. - workflow_dispatch
- A GitHub Actions trigger event that enables manual workflow runs from the GitHub web UI, GitHub CLI (
gh workflow run), or the GitHub API. Defined underon: workflow_dispatch:with an optionalinputs:block that specifies named parameters (string, boolean, choice, number, or environment type). All input values are received as strings in the workflow regardless of declared type — usefromJSON(inputs.name)to parse JSON string inputs andinputs.name == 'true'to evaluate boolean inputs. Supports up to 10 inputs per workflow. The event payload is available asgithub.event.inputs(legacy) orinputscontext (preferred). - actions/github-script
- A GitHub-maintained action that runs JavaScript in a workflow step with pre-authenticated access to the GitHub API via Octokit (
githubvariable), workflow utilities via@actions/core(corevariable), and the workflow run context (contextvariable). Used for JSON manipulation that is too complex for shell + jq: transforming API responses, building structured outputs from multiple API calls, conditional logic on parsed JSON, and reading/writing JSON files. Set step outputs withcore.setOutput(key, JSON.stringify(value)). Log at different levels withcore.info(),core.warning(), andcore.error(). Available atactions/github-script@v7.
FAQ
How do I use fromJSON() in GitHub Actions?
fromJSON(value) parses a JSON string and returns a typed value usable in expressions. Common uses: ${{ fromJSON(needs.build.outputs.matrix) }} in strategy.matrix to expand a JSON array into parallel jobs; ${{ fromJSON(inputs.config).env }} to access a field from a workflow_dispatch JSON input; ${{ fromJSON(steps.check.outputs.result).status == 'success' }} in an if: condition. The input must be a valid JSON string — generate it with jq -c to ensure compact, valid output. If the input is malformed, the expression fails and the step errors immediately, which is desirable for catching bad data early.
How do I create a dynamic matrix in GitHub Actions?
Three-step pattern: (1) In a setup job, generate a compact JSON array and write it to GITHUB_OUTPUT: echo "matrix=$(jq -c . targets.json)" >> $GITHUB_OUTPUT. (2) Expose it at the job level: outputs: matrix: ${{ steps.gen.outputs.matrix }}. (3) In the matrix job, declare needs: [setup] and use strategy: matrix: include: ${{ fromJSON(needs.setup.outputs.matrix) }}. The JSON must be an array of objects where each object defines the matrix dimension values for one parallel job. Use jq -c (compact output) to produce single-line JSON — multi-line values break the GITHUB_OUTPUT format and cause parse errors.
How do I set a JSON output in a GitHub Actions step?
Write compact JSON to GITHUB_OUTPUT: echo "result=$(jq -c . data.json)" >> $GITHUB_OUTPUT. The -c flag on jq produces compact single-line output, which is required because the KEY=VALUE format in GITHUB_OUTPUT does not support unescaped newlines. For multi-line JSON (e.g., JSON containing literal newlines in string values), use the heredoc delimiter syntax: echo "result<<EOF" >> $GITHUB_OUTPUT, then the value, then echo "EOF" >> $GITHUB_OUTPUT. Expose the step output at the job level with outputs: result: ${{ steps.step-id.outputs.result }} for downstream jobs to consume via needs.job-id.outputs.result.
How do I parse a JSON API response in GitHub Actions?
Use curl -sf to fetch and fail on HTTP errors, then pipe to jq: RESPONSE=$(curl -sf -H "Authorization: Bearer $TOKEN" https://api.example.com/data), then VERSION=$(echo "$RESPONSE" | jq -r .version). The -r flag on jq outputs raw strings without JSON quotes. Write extracted values to GITHUB_OUTPUT: echo "version=$VERSION" >> $GITHUB_OUTPUT. For GitHub API calls, use the built-in GITHUB_TOKEN secret — no configuration needed: curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/${{ github.repository }}/releases/latest. For complex transformations, use actions/github-script instead of shell + jq.
How do I pass JSON as a workflow_dispatch input?
Define a string input in the workflow_dispatch inputs: block with a valid JSON default: config: type: string default: '{"env":"staging"}'. All workflow_dispatch inputs are strings — use fromJSON(inputs.config) in expressions to access fields, or echo '${{ inputs.config }}' | jq -r .env in shell steps. When running via CLI: gh workflow run deploy.yml -f config='{"env":"production"}' — single-quote the JSON string to prevent shell interpretation. Validate the input before use: echo '${{ inputs.config }}' | jq empty exits non-zero if the JSON is malformed, failing the step early.
How do I debug GitHub Actions with toJSON()?
Add a debug step with if: always() so it runs even when prior steps fail: - name: Debug run: echo '${ ${{ toJSON(github) }} }' | jq .. Key contexts to dump: toJSON(github.event) for the webhook payload, toJSON(steps) for all step outputs and conclusions in the current job, toJSON(needs) for upstream job results and outputs, toJSON(inputs) for workflow dispatch inputs, and toJSON(matrix) for the current matrix combination. Pass context via env: to avoid shell quoting issues: env: CONTEXT: ${{ toJSON(github) }} then echo "$CONTEXT" | jq .. Alternatively, enable ACTIONS_STEP_DEBUG=true as a repository secret for verbose debug logging without modifying the workflow.
What is the maximum size for a GitHub Actions output?
Step output values written to GITHUB_OUTPUT are limited to 1 MB each. If your JSON payload exceeds this limit, use artifacts instead: upload with actions/upload-artifact in the producing job and download with actions/download-artifact in consuming jobs. Artifact files have a much higher size limit (up to 10 GB per artifact) and a configurable retention period (default 90 days). For JSON data shared between jobs, write to a temporary file, upload as an artifact with a deterministic name (deployment-config-${{ github.run_id }}), and download in the next job. GitHub secrets are limited to 64 KB per secret — do not use secrets for large JSON configs.
How do I use jq in GitHub Actions?
jq is pre-installed on all GitHub-hosted runners (ubuntu-latest, macos-latest, windows-latest) — no installation step is needed. Essential flags: -r outputs raw strings (no JSON quotes, required when writing to shell variables); -c outputs compact JSON (required for GITHUB_OUTPUT); -e exits non-zero if output is false/null (useful for validation); -n processes with no input (for constructing JSON from scratch). Pass shell variables safely: jq --arg name "$NAME" '.name == $name' (string) or jq --argjson count "$COUNT" '.count > $count' (JSON value). Build a matrix array from scratch: jq -cn '["node18","node20","node22"]'. Filter arrays: echo "$JSON" | jq -c '[.[] | select(.active)]'.
Further reading and primary sources
- GitHub Actions: Expressions — Official documentation for fromJSON(), toJSON(), and all built-in expression functions
- GitHub Actions: Using a matrix for your jobs — Complete guide to static and dynamic job matrices with strategy.matrix
- GitHub Actions: Workflow commands (GITHUB_OUTPUT) — Setting step outputs with GITHUB_OUTPUT and the heredoc delimiter syntax
- actions/github-script — Official repository for the github-script action with examples for JSON manipulation and API calls
- jq Manual — Complete jq language reference including filters, functions, and format strings for JSON processing