Writing your own rules

The package has three entry points. All are ESM and ship their own types.

ImportUse it for
workflow-lint/coreWriting rules and building tools: lint, resolveConfig, parseWorkflow, LintGraph, Fixer, and the Rule, RuleContext and Finding types
workflow-lint/rule-testerRuleTester, a fixture runner for rules. It needs vitest, which is an optional peer dependency
workflow-lintbuildProgram() and the reporters, for embedding the CLI

A rule

A rule is a meta block and a create function that returns handlers for selectors.

const rule = {
  meta: {
    id: 'acme/no-http-request',
    type: 'problem',
    class: 'quality',
    fixable: null,
    docs: { description: 'Acme forbids raw HTTP Request nodes.', recommended: 'error' },
    messages: { found: 'HTTP Request node "{{name}}" is not allowed.' },
  },
  create: (ctx) => ({
    'Node[type="n8n-nodes-base.httpRequest"]': (node) =>
      ctx.report({ node, messageId: 'found', data: { name: node.name } }),
  }),
};

Selectors

SelectorFires for
WorkflowThe workflow, before its nodes
Workflow:exitThe workflow, after everything else
NodeEvery node
ConnectionEvery connection
StickyNoteEvery sticky note

Filter by attribute with a string or a regular expression:

Node[type="n8n-nodes-base.if"]
Node[type=/Trigger$/]

Running it

The CLI does not load third-party rules from the config file yet. Run them through the API:

import { readFileSync } from 'node:fs';
import { lint, resolveConfig } from 'workflow-lint/core';

const config = resolveConfig(
  { settings: { n8nVersion: '2.38.3' }, rules: { [rule.meta.id]: 'error' } },
  new Map([[rule.meta.id, rule]]),
);

const path = 'workflow.json';
const { findings } = await lint({ text: readFileSync(path, 'utf8'), path }, config);

Testing it

import { RuleTester } from 'workflow-lint/rule-tester';

new RuleTester({ settings: { n8nVersion: '2.38.3' } }).run(rule, {
  valid: [{ workflow: workflowWithoutHttpNodes }],
  invalid: [{ workflow: workflowWithHttpNode, errors: [{ messageId: 'found' }] }],
});

workflow is a workflow object or a path to a JSON file. A case that supplies output must produce that document under --fix and lint clean afterwards.