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.
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).
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. 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.