Pkl 0.31 Release Notes

Pkl 0.31 was released on February 26th, 2026.
The latest bugfix release is 0.31.0. (All Versions)

The next release (0.32) is scheduled for July 2026. To see what’s coming in the future, follow the Pkl Roadmap.

Please send feedback and questions to GitHub Discussions, or submit an issue on GitHub.

Pkl is hosted on GitHub. To get started, follow Installation.

Highlights 💖

Power Assertions

Pkl’s test output and error output has been improved with power assertions (#1384, #1419)!

Pkl has two places that are effectively assertions. These are:

  • Type constraint expressions

  • Test facts

Currently, when these assertions fail, the error message prints the assertion’s source section. However, this information can sometimes be lacking. It tells you what the mechanics of the assertion is, but doesn’t tell you why the assertion is failing.

For example, here is the current error output of a failing typecheck.

–– Pkl Error ––
Type constraint `name.endsWith(lastName)` violated.
Value: new Person { name = "Bub Johnson" }

7 | passenger: Person(name.endsWith(lastName)) = new { name = "Bub Johnson" }

Just from this error message, we don’t know what the last name is supposed to be. What is name supposed to end with? We just know it’s some property called lastName but, we don’t know what it is.

In Pkl 0.31, the error output becomes:

–– Pkl Error ––
Type constraint `name.endsWith(lastName)` violated.
Value: new Person { name = "Bub Johnson" }

    name.endsWith(lastName)
    │    │        │
    │    false    "Smith"
    "Bub Johnson"

7 | passenger: Person(name.endsWith(lastName)) = new { name = "Bub Johnson" }

Now, we know what the expectation is.

This type of diagram is also added to test facts. When tests fail, Pkl emits a diagram of the expression, and the values produced.

For example, given the following test:

math.pkl
amends "pkl:test"

facts {
  local function add(a: Int, b: Int) = a * b
  local function divide(a: Int, b: Int) = a % b
  ["math"] {
    add(3, 4) == 7
    divide(8, 2) == 4
  }
}

The error output now includes a power assertion diagram, which helps explain why the test failed.

module math
  facts
    ✘ math
       add(3, 4) == 7 (math.pkl:9)
       │         │
       12        false
       divide(8, 2) == 4 (math.pkl:10)
       │            │
       0            false

0.0% tests pass [1/1 failed], 0.0% asserts pass [2/2 failed]

To learn more about this feature, consult SPICE-0026.

CLI Framework

Pkl 0.31 introduces a new framework for implementing CLI tools in Pkl (#1367, #1431, #1432, #1436, #1440, #1444).

The framework provides a way to build command line tools with user experience idioms that will be immediately familiar to users. CLI tools implemented in Pkl have largely the same capabilities as normal Pkl evaluation (i.e. writing to standard output and files), but this may be extended using external readers.

Commands are defined by extending the pkl:Command module:

bird-generator.pkl
extends "pkl:Command"

options: Options

class Options {
  /// Mapping of <bird>=<bird age> pairs defining the list of birds.
  @Argument
  birds: Mapping<String, Number>

  /// Aggregation function to apply to all bird ages.
  aggregate: *"sum" | "mean" | "count"
}

class Bird {
  /// Name of the bird.
  name: String

  /// Age of the bird in years.
  age: Number
}

birds: Listing<Bird> = new {
  for (_name, _age in options.birds) {
    new { name = _name; age = _age }
  }
}

result: Number =
  if (options.aggregate == "sum")
    birds.toList().fold(0.0, (result, bird) -> result + bird.age)
  else if (options.aggregate == "mean")
    birds.toList().fold(0.0, (result, bird) -> result + bird.age) / birds.length
  else
    birds.length

Commands are executed using the pkl run CLI subcommand:

$ pkl run bird-generator.pkl pigeon --aggregate=mean Pigeon=1 Hawk=8 Eagle=3
birds {
  new {
    name = "Pigeon"
    age = 1
  }
  new {
    name = "Hawk"
    age = 8
  }
  new {
    name = "Eagle"
    age = 3
  }
}
result = 4.0

Automatic CLI help is provided:

$ pkl run test.pkl -h
Usage: test.pkl [<options>] [<birds>]... <command> [<args>]...

Options:
  --aggregate=<count, mean, sum>  Aggregation function to apply to all bird ages.
  -h, --help                      Show this message and exit

Arguments:
  <birds>  Mapping of <bird>=<bird age> pairs defining the list of birds.

To learn more about this feature, consult the documentation and SPICE-0025.

Noteworthy 🎶

Syntax Highlighting

The Pkl CLI displays Pkl code in several locations: stack frames within errors messages, power assertions, and in the REPL. This code is now syntax highlighted to improve readability (#1385, #1409):

syntax highlighted output

CLI Support for Dependency Notation

The Pkl CLI now supports specifying modules using dependency notation (#1434, #1439). This is especially helpful for CLI commands defined in Packages:

$ pkl run @my-tool/cmd.pkl ...

This enhancement applies to the pkl eval, pkl run, pkl test, and pkl analyze imports commands. It also applies to the pkldoc, pkl-codegen-java, and pkl-codegen-kotlin tools.

Dependency notation only works for remote package dependencies. Local dependencies are not supported.

Property Converter Annotations

Pkl provides the BaseValueRenderers.converters mechanism for transforming values during rendering. Converters are flexible, but their design makes some transformations awkward. In particular, modifying property names during rendering required a lot of extra code.

The new ConvertProperty annotation adds a way to express parameterized per-property name and value transformations (#1333).

To learn more about this feature, consult SPICE-0024.

Additional new Pkl APIs for per-format property renaming will be added for many built-in renderers:

fmt.pkl
import "pkl:json"
import "pkl:yaml"

@json.Property { name = "foo_bar" }
@yaml.Property { name = "foo-bar" }
fooBar: String = "hello world"
$ pkl eval fmt.pkl -f json
{
  "foo_bar": "hello world"
}

$ pkl eval fmt.pkl -f yaml
foo-bar: hello world

Java API changes

New classes

New classes are introduced to the Java API.

  • org.pkl.core.CommandSpec

New methods

New methods are introduced to the Java API.

  • org.pkl.core.Evaluator.evaluateCommand

  • org.pkl.core.EvaluatorBuilder.setPowerAssertionsEnabled

  • org.pkl.core.EvaluatorBuilder.getPowerAssertionsEnabled

  • org.pkl.core.SecurityManager.resolveSecurePath

  • org.pkl.config.java.ConfigEvaluator.evaluateOutputValue

  • org.pkl.coznfig.java.ConfigEvaluator.evaluateExpression

Breaking Changes 💔

Things to watch out for when upgrading.

Removal of @argfile support

Prior versions of Pkl had an undocumented feature allowing inclusion of CLI arguments from files using @path/to/file. In order to support dependency notation on the CLI, @argfile support has been removed from Pkl.

Removal of Collection#transpose()

Prior versions of Pkl defined a transpose() method on the Collection class. This method was never implemented and threw an error when called. As it was never functional, it has been removed entirely without a deprecation warning (#1437).

Miscellaneous 🐸

  • Improve formatting of imports to keep surrounding comments (#1424).

  • Add support for evaluating module output and expressions to ConfigEvaluator (#1297).

  • The pkl format --write command now exits successfully when formatting violations are found and updated (#1340).

  • Add pkl-bom module to aid in aligning Pkl Java dependencies (#1390).

  • Improve error message when writing PklProject.deps.json fails (#1405).

  • Add information about Annotations to the language reference (#1427).

  • Improved usability for the org.pkl.formatter.Formatter Java API (#1428).

Bugs fixed 🐜

The following bugs have been fixed.

  • Function.toString() returns incorrect result (#1411).

  • Failure when --multiple-file-output-path is a symlink (#1389).

  • The module type in a non-final module has default value of type Dynamic (#1392).

  • The module type is cached incorrectly in some cases (#1393).

  • A possible race condition involving symlinks could bypass --root-dir during module and resource reading (#1426).

  • pkl format produces internal stack traces when lexing fails (#1430).

  • super access expressions are parsed incorrectly inside the spread operator (#1364).

  • Modules and resources with jar:file: URIs were not properly sandboxed by --root-dir (#1442).

Contributors 🙏

We would like to thank the contributors to this release (in alphabetical order):