Files
Ruben Fiszel a372ae0c04 fix(cli): lint against the checkout's schema, not the published validator (#10418)
* fix(cli): lint against the checkout's schema, not the published validator

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(cli): mirror the permissioned_as exclusion into agent guidance

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: version windmill-yaml-validator with the release, publish by hand

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: generate schemas with the validator's own yaml parser

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(cli): declare ajv, no longer reaching tests via the validator

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: fail schema generation on a spec YAML syntax error

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:33:35 +02:00
..

Windmill YAML Validator

A TypeScript-based YAML validator for Windmill flow, schedule, and trigger files.

Overview

The windmill-yaml-validator provides runtime validation for Windmill YAML files. It is used by editor integrations to show validation errors while editing:

  • flow.yaml / flow.yml
  • *.schedule.yaml / *.schedule.yml
  • *.{http|websocket|kafka|nats|postgres|mqtt|sqs|gcp}_trigger.yaml (or .yml)

Features

  • Unified validation API: One validator class for flow/schedule/trigger files
  • Schema-based validation: Uses OpenFlow and backend OpenAPI-derived schemas
  • Detailed error reporting: Returns comprehensive error information with specific paths to invalid fields

Installation

npm install windmill-yaml-validator

Usage

Basic Validation

import { WindmillYamlValidator } from "windmill-yaml-validator";

const validator = new WindmillYamlValidator();

const flowYaml = `
summary: Test Flow
value:
  modules: []
`;

const flowResult = validator.validate(flowYaml, { type: "flow" });

const scheduleYaml = `
schedule: "0 0 12 * * *"
timezone: "UTC"
enabled: true
script_path: "f/jobs/daily_sync"
is_flow: false
`;

const scheduleResult = validator.validate(scheduleYaml, { type: "schedule" });

const triggerYaml = `
script_path: "f/triggers/http_handler"
is_flow: false
route_path: "api/webhook"
request_type: "sync"
authentication_method: "none"
http_method: "post"
is_static_website: false
workspaced_route: false
wrap_body: false
raw_string: false
`;

const triggerResult = validator.validate(triggerYaml, {
  type: "trigger",
  triggerKind: "http",
});

console.log(flowResult.errors, scheduleResult.errors, triggerResult.errors);

Target Inference by Filename

import {
  WindmillYamlValidator,
  getValidationTargetFromFilename,
} from "windmill-yaml-validator";

const validator = new WindmillYamlValidator();
const target = getValidationTargetFromFilename(
  "f/webhooks/order_created.http_trigger.yaml"
);

if (target) {
  const result = validator.validate(fileContents, target);
  console.log(result.errors);
}

Error Handling

const invalidYaml = `
summary: 123  # Should be a string
value:
  modules:
    - id: step1
      value:
        type: rawscript
        language: invalid_language  # Invalid enum value
`;

const result = validator.validate(invalidYaml, { type: "flow" });

result.errors.forEach((error) => {
  console.log(`Error at ${error.instancePath}: ${error.message}`);
  // Example output:
  // Error at /summary: must be string
  // Error at /value/modules/0/value/language: must be equal to one of the allowed values
});

API

WindmillYamlValidator

Main validator class for Windmill YAML validation.

Constructor

new WindmillYamlValidator();

Initializes AJV validators for flow, schedule, and trigger schemas.

Methods

validate(doc: string, target: ValidationTarget)

Validates a YAML document against the selected target schema.

Parameters:

  • doc (string): YAML document string
  • target (ValidationTarget):
    • { type: "flow" }
    • { type: "schedule" }
    • { type: "trigger", triggerKind: "http" | "websocket" | "kafka" | "nats" | "postgres" | "mqtt" | "sqs" | "gcp" | "email" }

Returns:

{
  parsed: YamlParserResult<unknown>;  // Parsed YAML with source pointers
  errors: ErrorObject[];               // Array of validation errors (empty if valid)
}

Throws:

  • Error if doc is not a string

getValidationTargetFromFilename(path: string)

Infers validation target from file naming conventions. Returns null for unsupported files.

Development

Building

npm run build

The build process:

  1. Runs npm run gen (scripts/gen-schemas.js) to generate:
    • src/gen/openflow.json
    • src/gen/schedule.json
    • src/gen/triggers/*.json
  2. Removes discriminator mappings (not supported by AJV)
  3. Compiles TypeScript to JavaScript

Testing

npm install   # cli/ installs this package with --omit=dev, so jest may be absent
npm test

Run tests in watch mode:

npm test:watch

Relationship with the CLI

wmill lint does not use the published package: it imports src/index.ts directly, and cli's preinstall regenerates src/gen from the repo's OpenAPI specs. Lint therefore always validates against the schema of the commit it is built from, and local changes here are picked up with no publish or link step:

# In cli/
bun run src/main.ts lint

The published package still exists for editor integrations. CI does not publish it — .github/change-versions.sh moves its version with each release, but pushing that version to npm is a manual ./publish.sh.

Schema Generation

The validator uses a JSON schema generated from the OpenAPI specification:

npm run gen

This script:

  • Converts openflow.openapi.yaml and backend/windmill-api/openapi.yaml into JSON
  • Removes discriminator mappings for AJV compatibility
  • Removes the ToolValue discriminator entirely (see below)
  • Generates standalone schedule/trigger schemas for CLI file shape

Why Remove Discriminators?

The OpenFlow schema uses OpenAPI discriminators for efficient type resolution in oneOf schemas. However, AJV's discriminator support has limitations:

  1. Discriminator Mappings: Not fully supported by AJV, so they are removed from all schemas
  2. ToolValue Discriminator: Completely removed because FlowModuleTool uses allOf composition, which prevents AJV from finding the discriminator property (tool_type) at the expected location

Impact: Without discriminators, AJV falls back to standard oneOf validation, which:

  • Tests each alternative until one matches
  • Is slightly slower but still performant for our use case
  • Provides the same validation correctness
  • Works correctly with complex schema compositions like allOf

Breaking Change

FlowValidator and validateFlow() were replaced by WindmillYamlValidator and validate(doc, target).