Custom rules
The rule API and code format — how built-in rules are defined and how organization-specific rules fit in.
Every rule — built-in or not — is a single definition. That definition is the source for the runtime message, the
why/fix text and docs URL printed with each finding, and (for built-in rules) flowpact explain and the generated rule pages
on this site.
/** Jobs that deploy to production must depend on the approval job. */
const prodNeedsApproval = {
code: 'ACME601',
name: 'prod-deploy-needs-approval',
category: 'structure',
defaultSeverity: 'error',
docsUrl: 'https://wiki.acme.dev/ci/ACME601',
docs: {
summary: 'Jobs that deploy to production must depend on the approval job.',
why: 'Production deploys without the approval gate bypass change management.',
fix: 'Add `approve` to the job’s `needs:`.',
},
check(ctx) {
for (const wf of ctx.index.project.workflows.values()) {
for (const job of Object.values(wf.jobs)) {
const deploysProd = job.with.environment?.value === 'production';
if (deploysProd && !job.needs.some((n) => n.id === 'approve')) {
ctx.report({
message: `jobs.${job.id} deploys to production without needing "approve"`,
loc: job.loc,
symbol: `${wf.path}#jobs.${job.id}`,
});
}
}
}
},
};
export default [prodNeedsApproval];A rule is a plain object, so a plugin needs no imports. Built-in rules are written the same way in TypeScript, wrapped
in defineRule(...) for type checking. A plugin written in TypeScript gets the RuleDefinition type from the flowpact
package (see TypeScript in the API).
Besides message, loc and symbol, ctx.report takes fix (advice for this finding instead of the rule's) and
severity, which reports one finding below the rule's severity. It can only lower it: a finding never exceeds the
severity the rule is configured with.
Two optional fields shape where and how a rule speaks:
generatedFiles: 'skip'drops the rule's findings located in generated files (gh-aw*.lock.yml, aDO NOT EDITheader). Use it for rules about a file's own internals, such as dead declarations or style; the default,'report', suits checks of calls between units.docs.scopedescribes what the rule deliberately leaves out. It appears inflowpact explain(and on the built-in rule pages), but not with each finding.
Codes
Codes have the form <PREFIX><category digit><two digits>:
| Digit | Category | Digit | Category |
|---|---|---|---|
1 | inputs | 6 | structure |
2 | secrets | 7 | hygiene |
3 | outputs | 8 | contracts |
4 | matrix | 9 | config |
5 | expressions |
The registry enforces the format and that the digit matches the declared category, rejects duplicate codes and names,
reserves the FP prefix for built-in rules, and requires third-party rules to bring their own docsUrl.
What a rule can use
ctx.index is the data-flow graph:
ctx.index.units(),project.workflows,project.actions— the IR with exact locations;callSites,callersOf(path),actionUses,usersOf(path)— who calls whom;usagesOf(symbol)— every place a symbol is read, with the location of the reference and where the value flows next;ctx.matrix(unit, job)— the expanded matrix (combinations, keys, exactness);ctx.config— the resolved configuration;ctx.contracts— the contract comparison (only inflowpact check).
ctx.report() takes the message, loc and optional symbol, related locations, combos and an instance-specific
fix. A milder variant of the problem can pass severity: 'warning' or 'info': it lowers that finding below the
rule's severity, never raises it, and a lower severity set in the config still wins.
Loading plugins
List the module under plugins: in the config:
plugins:
- ./ci/flowpact-rules.mjs
rules:
prod-deploy-needs-approval: warning # plugin rules are configured like built-in ones- Paths are relative to the repository root; the module is loaded with Node's
import(), so it must be JavaScript (.mjs, or.jsin an ESM package). - It default-exports a rule or an array of rules, or exports them as
rules. - The CLI also loads plugins given with
--plugin <file>(relative to the working directory, repeatable), and the action those in itsplugininput (relative to the action's directory), such as organization rules shipped with a copy of the action.--no-pluginsand the action'spluginsinput skip the plugins the config lists, but not those. flowpact lint,flowpact checkand the GitHub Action run plugin rules next to the built-in ones, so they can be configured underrules:, accepted with overrides and selected with--only.- A plugin that is missing, fails to load, exports no rules, or registers an invalid rule (wrong code format,
FPprefix, nodocsUrl, duplicate code or name) stops the run with exit code2.
Each finding links to the rule's docsUrl, so point it at your own documentation.