Skip to content

Configuration File

This page is the canonical reference for Thymian configuration.

Thymian resolves configuration in this order:

  1. If you pass --config <path>, Thymian uses that file.
  2. Otherwise, Thymian looks for thymian.config.yaml in --cwd (or your current directory).
  3. If no file is found, Thymian uses built-in defaults.

See Configuration Schema for the complete list of supported keys, types, defaults, and descriptions.

After loading config, CLI flags can override parts of it. Common overrides include:

  • --spec <type:path> overrides specifications
  • --traffic <type:path> overrides traffic
  • --report <type:path> overrides reports (used by thymian report convert)
  • --target-url <url> overrides targetUrl
  • --rule-set <name> extends ruleSets at runtime (appends and deduplicates)
  • --rule-severity <off|error|warn|hint> overrides ruleSeverity
  • --log-level <trace|debug|info|warn|error|silent> overrides logLevel
  • --debug effectively overrides logLevel to debug
  • --verbose effectively overrides logLevel to info
  • --autoload/--no-autoload controls plugin autoloading

thymian generate config creates a config based on built-in defaults plus your selected specification(s).

Typical generated baseline:

specifications:
- type: openapi
location: ./openapi.yaml
traffic: []
ruleSets:
- '@thymian/rules-rfc-9110'
- '@thymian/rules-api-description-validation'
ruleSeverity: error
rules: {}
plugins:
'@thymian/plugin-http-linter': {}
'@thymian/plugin-openapi': {}
'@thymian/plugin-request-dispatcher': {}
'@thymian/plugin-sampler': {}
'@thymian/plugin-reporter':
options:
formatters:
markdown: {}
'@thymian/plugin-http-tester': {}
'@thymian/plugin-http-analyzer': {}
'@thymian/plugin-har': {}
'@thymian/plugin-spectral': {}
specifications:
- type: openapi
location: ./openapi.yaml
ruleSets:
- '@thymian/rules-rfc-9110'
plugins:
'@thymian/plugin-http-linter': {}
'@thymian/plugin-openapi': {}
'@thymian/plugin-reporter':
options:
formatters:
markdown: {}
specifications:
- type: openapi
location: ./openapi.yaml
targetUrl: http://localhost:3000
ruleSets:
- '@thymian/rules-rfc-9110'
plugins:
'@thymian/plugin-openapi': {}
'@thymian/plugin-request-dispatcher': {}
'@thymian/plugin-sampler': {}
'@thymian/plugin-http-tester': {}
'@thymian/plugin-reporter':
options:
formatters:
markdown: {}

reports declares the pre-generated external reports that thymian report convert imports; the optional specifications entries let the converter map findings onto your API description. --report flags replace the reports entries, exactly like --spec replaces specifications.

reports:
- type: spectral
location: ./report.json
specifications:
- type: openapi
location: ./openapi.yaml
plugins:
'@thymian/plugin-openapi': {}
'@thymian/plugin-spectral': {}
'@thymian/plugin-reporter':
options:
formatters:
markdown: {}

See Importing Spectral results for the full flow.

traffic:
- type: fixture
location: static
ruleSets:
- '@thymian/rules-rfc-9110'
plugins:
'@thymian/plugin-http-analyzer': {}
'@thymian/plugin-reporter':
options:
formatters:
markdown: {}

@thymian/plugin-reporter writes one file per configured formatter. Where those files land is a plugin-level setting, not a per-formatter one: reportsDir names the base directory — a relative path is resolved against --cwd, an absolute path is used as-is — and it defaults to .thymian/reports. Leave it out to take the default; an empty or whitespace-only reportsDir fails validation rather than scattering run directories through the working directory.

Every report gets its own directory beneath that base, and the files inside keep a stable name, so consecutive runs never overwrite each other:

<reportsDir>/<created-at>-<report-id>/report.<ext>
.thymian/reports/2026-08-25T10-30-00-123Z-a1b2c3d4/report.md

<created-at> is the report’s creation timestamp with : and . replaced by - (no colons, so the name is valid on Windows too), and <report-id> is the first eight characters of the report id. Every formatter in the same run sees the same report, so the markdown, CSV, and JSON output of one run lands side by side in that one directory as report.md, report.csv and report.json — correlating them is opening the directory.

The rule holds everywhere, including thymian serve: one reporter instance serves the whole session, but each workflow emits its own report, so each workflow gets its own run directory.

plugins:
'@thymian/plugin-reporter':
options:
# Optional. Defaults to .thymian/reports.
reportsDir: build/reports
formatters:
markdown: {} # build/reports/<created-at>-<report-id>/report.md
csv: {} # build/reports/<created-at>-<report-id>/report.csv
json: {} # build/reports/<created-at>-<report-id>/report.json
Formatter Output file Use it for
markdown <reportsDir>/<created-at>-<report-id>/report.md Human review, PR comments, CI job summaries
csv <reportsDir>/<created-at>-<report-id>/report.csv Tabular analysis in spreadsheets or BI tools
json <reportsDir>/<created-at>-<report-id>/report.json Automation and post-processing of the full report data

Formatters take no options of their own — markdown: {} selects the formatter, nothing more. There is no way to pin one fixed output file, because a fixed file is exactly what makes a second run destroy the first one’s report.

A downstream step therefore matches a pattern rather than a path: a CI job that uploads or reads the output should glob <reportsDir>/*/report.<ext> — for example .thymian/reports/*/report.md.

# Upload every run directory the job produced.
- uses: actions/upload-artifact@v7
with:
name: thymian-reports
path: .thymian/reports/*/report.*

A step that needs exactly one file — posting the Markdown as a PR comment, piping the JSON into a script — has two options. Either take the newest run: a run directory name starts with the report’s creation timestamp, so the names sort chronologically and the last one is the latest run.

Terminal window
report="$(ls -1d .thymian/reports/*/ | tail -1)report.md"

Or point reportsDir at a directory that is empty when the run starts (reportsDir: build/reports in a fresh workspace, or a step that clears it first), so the glob can only ever match once. In a CI job with a cached or reused workspace, clearing it is a step of its own.

The per-formatter path option was removed. A formatter that still sets it fails config validation, because formatter options take no properties at all. To migrate:

  1. Delete the path key — markdown: {} is the whole formatter configuration now.
  2. If it pointed somewhere other than the default, set the plugin-level reportsDir to that directory instead. It is one base for every formatter, not a per-formatter file.
  3. Where a pipeline read the fixed file, glob <reportsDir>/*/report.<ext> instead.
plugins:
'@thymian/plugin-reporter':
options:
formatters:
markdown:
path: build/reports/report.md # ❌ before
plugins:
'@thymian/plugin-reporter':
options:
reportsDir: build/reports # ✅ after
formatters:
markdown: {} # build/reports/<created-at>-<report-id>/report.md

The reporter also checks that reportsDir can be created before the run starts, whenever at least one formatter is configured. A base directory it cannot create fails the run with UnusableReportsDirectoryError rather than letting a green run produce no files. With formatters: {} nothing is checked and no directory is created.

A failure while a report is being written is deliberately different: it is logged as an error on stderr and leaves that one file missing, but it does not fail the run. The findings already exist by then, so an incomplete report beats losing them.

Pick markdown when a person or AI agent reads the result — in a pull request, a CI job summary, or locally:

  • Review in GitHub: the document uses inline HTML (<details> blocks, a <sub> legend, colored <span> counts), so it pastes straight into a PR comment or $GITHUB_STEP_SUMMARY.
  • Triage at a glance: a severity roll-up, a per-run outcome table, and lint findings grouped under the endpoint they belong to.
  • Failures only: passed test cases and passed executions without findings are left out, so the document stays focused on what needs attention.

Locations are resolved to the transaction’s Selector, such as POST /orders (application/json) -> 201, and the raw request/response of a failed test step is tucked into a nested <details> block. Table cells are single-line, so multi-line messages are flattened, and expected/actual values are folded into the message rather than kept as fields.

Pick csv when you want to sort, filter, or pivot results in a spreadsheet or BI tool:

  • Tabular analysis: thirteen fixed columns, one record per line, nothing nested to unwrap.
  • Complete outcome tally: unlike markdown, every execution gets a row — passed, failed, and skipped alike — so you can compute pass rates instead of only counting failures.
  • Two row types: a row_type=execution row carries the outcome (status and resolved severity), and each row_type=finding row beneath it carries one assertion or informational detail.

The columns are run_id, run_type, tool, rule_id, location, row_type, status, severity, finding_kind, finding_id, title, message, and detail. Structured values are flattened into detail as key=value pairs (expected=200; actual=404). HTTP transactions are not included.

Pick json when code — rather than a reader — consumes the output, and you need the complete report:

  • Post-processing and integrations that would otherwise have to parse markdown or CSV.
  • Archiving a run so its full result set can be re-examined later.
  • Detail the other formatters omit, such as passed executions, finding identifiers, run-level artifacts and invocations, and the serialized Thymian format graph that locations reference.

The file contains the canonical report payload exactly as Thymian emitted it on the core.report event, with no presentation transforms applied. The top level is always a JSON array of report objects, holding the one report that run directory belongs to — a session that produces several reports produces several directories, each with its own one-element array.

Locations are stored as references, not rendered endpoint strings: a thymianFormat location carries elementId/elementType that you resolve against the run’s entry in the report’s thymianFormat map (selected by ToolRun.thymianFormatVersion).

If config parsing or schema validation fails, the CLI exits with code 2 and prints an error.

Common issues:

  1. Unsupported file extension when loading with --config
  2. Invalid YAML/JSON syntax
  3. Invalid value types (for example non-array ruleSets)
  4. Unknown or misspelled top-level keys — the schema only allows the documented keys and rejects anything else
  5. Missing specification with commands that require one
  • Prefer thymian generate config to bootstrap a correct baseline.
  • Keep plugin configuration minimal first, then add plugin options incrementally.
  • Use --spec for one-off checks without editing files.
  • Keep ruleSeverity: error in CI unless you explicitly want warnings/hints to gate builds.