Configuration
Configure severities, limits, ignored paths, overrides, matrix shapes, generated files and plugins in .github/flowpact/flowpact.config.yml, and share defaults across repositories with a base config.
flowpact works without configuration. To change behaviour, create
.github/flowpact/flowpact.config.yml (or pass --config <file>):
# yaml-language-server: $schema=https://ciatlas.github.io/flowpact/schemas/config/v1.json
version: 1
# owner/repo of this repository, so `uses: owner/repo/.github/workflows/x.yml@ref` resolves locally.
# Detected from GITHUB_REPOSITORY or .git/config when omitted.
repository: acme/platform
rules:
# by code or by name; values: error | warning | info | off
FP104: error
secrets-inherit: off
needs-without-data: info # opt-in rule
limits:
nestingDepth: 10 # FP602: workflows in one call chain, counting the top-level workflow
maxInputs: 30 # FP605: workflow_call inputs before suggesting to group them
ignore:
- .github/workflows/experimental/ # path prefix
- .github/workflows/legacy-*.yml # glob (* within a segment, ** across)
overrides:
- rule: unused-input
target: .github/workflows/pipeline.yml#inputs.legacy-flag
reason: Read by the external release dispatcher; remove after the migration (JIRA-123)
expires: 2026-12-31
owner: '@platform-team'
matrixShapes:
.github/workflows/tests.yml#e2e:
keys: [os, browser]
generated:
include: [.github/workflows/rendered-*.yml] # generated, but without a header comment
exclude: [.github/workflows/docs.yml] # written by hand, despite its header
plugins:
- ./ci/flowpact-rules.mjsThe config is validated strictly: unknown keys and invalid values are reported with their path (exit code 2). An
unknown code with the built-in prefix (FP and a digit, such as FP999) or a typo of a loaded rule is an error too,
with a suggestion; other unknown rule names are ignored with a warning, since they may
belong to a plugin that is not loaded.
Editor autocomplete
The schema is published at /schemas/config/v1.json. The comment on the first line of the
example enables completion, hover docs and validation in editors that use the YAML language server (VS Code with the
Red Hat YAML extension, JetBrains IDEs, Neovim):
# yaml-language-server: $schema=https://ciatlas.github.io/flowpact/schemas/config/v1.jsonOverrides
rules: and ignore: change a rule or a path everywhere. An override accepts one specific finding, with a reason
that stays next to it in the repository:
overrides:
- rule: unused-input # code (FP104) or name
target: .github/workflows/pipeline.yml#inputs.legacy-flag # the symbol the finding is about
reason: Read by the external release dispatcher; remove after the migration (JIRA-123)
expires: 2026-12-31 # optional, YYYY-MM-DD
owner: '@platform-team' # optional
- rule: remote-unverified
file: .github/workflows/release.yml # every finding of this rule in this file
reason: Calls into acme/shared-workflows are verified in that repository| Key | Required | Description |
|---|---|---|
rule | yes | Rule code or name. See Rules that are not loaded for names no loaded rule has. |
target | target or file | The finding's symbol id. * matches within a path segment, ** across segments, e.g. .github/workflows/*.yml#inputs.legacy-*. |
file | target or file | The file the finding is reported in: a path, a path prefix or a glob. |
reason | yes | Why the finding is accepted (at least 10 characters). |
expires | no | YYYY-MM-DD (a UTC date). The override is honored through the end of that day in UTC; from the next day the finding is reported again. |
owner | no | Who owns the exception, shown in expiry messages. |
Either target or file is required, so an override cannot silence a rule everywhere (use rules: for that). When
both are set, both must match.
Finding the symbol. Every finding has a symbol in JSON output — copy it into target:
flowpact lint --format json | jq -r '.findings[] | "\(.code) \(.symbol)"'What happens to accepted findings. They are removed from the report and from the error/warning/info counts, so
they do not fail the build. They are not lost: JSON output lists them under suppressed, each with the override that
accepted it (index, reason, expires, owner), and summary.suppressed counts them.
Keeping overrides honest. Three rules check the overrides themselves (they run after overrides are applied, so they cannot be overridden):
| Code | Severity | Reported when |
|---|---|---|
FP901 override-expired | error | The expires date has passed. The findings it accepted are reported again. |
FP902 override-unused | warning | The override matches no finding — the problem was fixed, or the target is misspelled. |
FP903 override-expiring-soon | info | It expires within 14 days and still matches findings. |
Overrides apply to every rule, including the contract rules — but an intended interface change is
better recorded by regenerating the contracts than by overriding FP803.
Matrix shapes
A matrix computed at runtime (matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}) cannot be expanded statically,
so by default flowpact reports it as unverifiable (FP403) and does not check its matrix.* reads. If you know which keys
the generated matrix has, declare them:
matrixShapes:
# <workflow path>#<job id>
.github/workflows/tests.yml#e2e:
keys: [os, browser, shard]flowpact then checks every matrix.* read in that job against the declared keys and reports unknown ones as FP404
(for example matrix.brower); FP403 is no longer reported for the job. The values of the keys stay unknown, so
per-combination checks (FP401, FP402) do not apply. The declaration is used only for jobs whose matrix (or
include) is actually computed at runtime.
Generated files
Some workflows are written by tools rather than people: GitHub Agentic Workflows
compiles *.md files into *.lock.yml workflows, and some repositories render their workflows from templates. Editing
such a file does not fix anything — the next generation overwrites it — so flowpact still checks how it calls other
units and how other units call it, but does not report on its internals.
A workflow or action file counts as generated when its name ends in .lock.yml, or when the comment block at the top
of the file (within its first 10 lines) says so in a generator's words:
DO NOT EDITorDO NOT MODIFYin capitals (DO NOT EDIT.,DO NOT EDIT MANUALLY), ordo not edit this file,do not edit manually,do not edit by hand;@generated;This file was generated,This workflow is automatically generated,This is an auto-generated file;- a comment that starts with
Generated by <tool>,Generated from <file>,Code generated by <tool>orAuto-generated file.
# This file was automatically generated by gh-aw. DO NOT EDIT.
name: TriageA comment that only talks about generated things does not count: # Generated docs are published by this workflow,
# Builds the autogenerated API client or # Please do not edit the deploy keys leave the file hand-written.
flowpact lint -v lists the files it treats as generated, and why. When it guesses wrong, say so in the config;
paths and globs match the file (.github/actions/x/action.yml) or, for an action, its directory:
generated:
include: # generated, though nothing in the file says so
- .github/workflows/rendered-*.yml
exclude: # written by hand, whatever the header or name says
- .github/workflows/docs.ymlA file listed under exclude is never treated as generated, even when it also matches include.
In a generated file, these rules are not reported — dead declarations and style advice whose only fix is an edit to the file:
| Code | Name |
|---|---|
FP104 | unused-input |
FP105 | optional-input-no-default-in-condition |
FP203 | unused-secret |
FP204 | secrets-inherit |
FP303 | unused-output |
FP304 | step-output-never-written |
FP604 | needs-without-data |
FP605 | large-interface |
FP607 | unreferenced-reusable-workflow |
Every other rule still applies: invalid YAML or expressions, schema errors, and every check of a call between units —
a generated workflow that calls one of your reusable workflows without a required input (FP101), or a hand-written
workflow that passes an unknown input to a generated one (FP102). Values that a hand-written unit receives from or
hands to a generated one are traced as usual. To drop every finding in generated files, ignore them:
ignore:
- .github/workflows/*.lock.ymlThe report counts the findings it leaves out (summary.skippedInGenerated in --format json, and a line under the
summary). An override written for one of them, from before flowpact skipped generated files, now matches nothing:
FP902 says so, and the override can be deleted.
Plugins
Organization-specific rules can be loaded from JavaScript modules:
plugins:
- ./ci/flowpact-rules.mjsPaths are relative to the repository root. A module default-exports a rule or an array of rules (or exports them as
rules). Plugin rules need their own code prefix and a docsUrl; see Custom rules for the rule
format. Plugins are loaded by flowpact lint, flowpact check and flowpact generate, and by the GitHub Action.
Plugins run code from the repository
Loading a plugin executes it, with the permissions of whoever runs flowpact. Only run flowpact with plugins on code you trust.
The GitHub Action skips plugins on pull_request_target and workflow_run events unless its plugins input is set
to true; the CLI skips them with --no-plugins.
Rules that are not loaded
rules: and overrides: entries for a rule that is not loaded in a run are ignored for that run and listed in a
warning. That happens when plugins are skipped, or when a repository configures an
organization rule that only its organization's wrapper loads: the entry takes
effect in the wrapper, and plain flowpact lint or the editor ignore it. What is still a config error, so a typo is
not hidden:
- a code with the built-in prefix (
FP999,FP-101), or a built-in code with a character replaced (FPl01); - a code one edit from a loaded code with the same prefix (
ACME610beside a loadedACME601); - another spelling of a loaded rule's name (
unusedInput), or one at most two edits from it (unused-inptu).
A code with another prefix (AC201, DP201) or a name that adds a word to a loaded one (no-secrets-inherit) belongs
to a plugin and is ignored with the warning.
Base config
A base config holds defaults for many repositories, such as an organization's. It is passed with the CLI's
--base-config <file> or the action's base-config input, usually by an organization's
copy of the action or wrapper, and each repository's own config goes on top of it:
rules:
FP105: error # stricter than the default everywhere
needs-without-data: info # an opt-in rule, on for everyone
acme-deploy-env: error # an organization rule, loaded with --plugin
ignore:
- .github/workflows/generated/
impact:
declaredBy: labelsrules:
FP105: warning # this repository wins
ignore:
- .github/workflows/legacy-*.ymlflowpact lint --base-config acme.base.yml| Key | In a base config | Combined with the repository's |
|---|---|---|
rules, limits, impact | allowed | key by key; the repository's value wins (impact.types and impact.labels entry by entry) |
ignore, generated | allowed | both lists apply |
repository, overrides, matrixShapes, impact.publish | config error | they name files of one repository |
plugins | config error | plugins run code; organization rules load with the action's plugin input or --plugin |
The repository's config still works on its own: run without the base config, as locally and in the editor, flowpact
uses the repository's config and the defaults. The base config's path is relative to the working directory
(--base-config) or to the action's own directory (the base-config input), and the banner shows it (config .github/flowpact/flowpact.config.yml · base acme.base.yml). A repository's config cannot
point at a base config itself; see why.
Reference
| Key | Type | Default | Description |
|---|---|---|---|
$schema | string | — | JSON Schema reference for editors (the # yaml-language-server comment works too). |
version | 1 | 1 | Config schema version. |
repository | owner/repo | detected | Lets same-repository owner/repo/...@ref references resolve locally. |
rules | map of code or name → error | warning | info | off | {} | Severity per rule. |
limits.nestingDepth | positive integer | 10 | Maximum workflows in one call chain (FP602). |
limits.maxInputs | positive integer | 30 | workflow_call inputs before FP605. |
ignore | list of paths or globs | [] | Findings in matching files are suppressed. |
overrides | list of {rule, target?, file?, reason, expires?, owner?} | [] | Accepted findings, each with a reason and an optional expiry date (details). |
matrixShapes | map of <workflow path>#<job id> → { keys: [...] } | {} | Declared keys of runtime-computed matrices (details). |
generated.include | list of paths or globs | [] | Workflows and actions that are generated, even without a marker (details). |
generated.exclude | list of paths or globs | [] | Workflows and actions written by hand, even with a marker or a .lock.yml name; wins over include. |
impact.publish | list of paths or globs | reusable workflows (except _-prefixed files) and the root action.yml | Units other repositories use (impact mode). Their outputs are not reported as unused (FP303); list actions in subdirectories (owner/repo/restore@v1) here too. |
impact.declaredBy | explicit | title | labels | explicit when given, else title | The authoritative source of the declared impact. |
impact.labels | { major, minor, patch, none } | semver:* | Label names that declare each level. |
impact.types | map of type → level | feat: minor, fix / perf: patch | Conventional Commits types; others declare none, ! declares major. |
impact.uncertain | warn | fail | warn | Whether changes flowpact cannot fully resolve count towards the required impact. |
plugins | list of paths | [] | JavaScript modules exporting extra rules (details). |