Pkl 0.32 Release Notes

Pkl 0.32 was released on July 8th, 2026.
The latest bugfix release is 0.32.0. (All Versions)

The next release (0.33) is scheduled for October 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.

This is the final release that distributes binaries for macOS/amd64.

Highlights 💖

New data type: Reference

This release introduces a new data type called Reference (#1354, #1691, #1692, #1693, #1695).

This API is experimental. We are looking for feedback, and will aim to make this ready for production use in the next release.

A reference represents something whose actual value is unknown to Pkl, but nevertheless its type is checked. The purpose of this data type is to improve how Pkl can be used to configure systems like GitHub Actions, Pulumi, Terraform, and more.

For example, let’s assume that some system accepts YAML configuration that looks like so:

taskA:
  uses: some-task

taskB:
  uses: some-other-task
  input: '${ taskA.output.number }'

In terms of YAML, '${ taskA.output.number }' is just a string. However, semantically, it is an expression in this target system, where taskA.output.number conveys some type.

Today, the only way to treat these values in Pkl is to also describe these as strings. This introduces many shortcomings:

  1. It leaves users vulnerable to syntax errors in this system.

  2. It’s hard to validate these beyond that they are a string.

  3. Pkl has no understanding of the relationship between taskA and taskB.

With references, the above can be modeled in Pkl also as plain lookups:

taskA {
  uses = "some-task"
}

taskB {
  name = "some-other-task"
  input = taskA.output.number
}

Library authors describe types as ref.Reference<D, T>, where D is the domain, and T is the referent type.

import "pkl:ref"

class MyDomain extends ref.Domain { (1)
  function renderReference(ref: ref.Reference<MyDomain, Any>): String =
    "${ " + ref.getData() + "." + ref.getPath().join(".") + " }"
}

typealias MyReference<T> = ref.Reference<MyDomain, T> (2)

class MyTask {
  input: MyReference<Number>
}
1 References exist inside a domain. The domain defines how these references should be stringified.
2 A typealias can be used to improve ergonomics when declaring references that share the same domain.

Pkl will check that type arguments match up:

  • The referent type must line up.

    MyReference<Int> is not assignable to MyReference<String>.

  • The domain must line up.

    ref.Reference<MyDomain, Int> is not assignable to ref.Reference<OtherDomain, Int>.

Additionally, references give you synthetic members.

class Bird {
  name: String
}

bird: MyReference<Bird>

birdName = bird.name (1)
1 Gives a MyReference<String>, because class Bird has property name: String

The synthetic member contains metadata about the path that was used to access it (.name in this case). This can be used to stringify this reference in that target domain.

To read more about references, consult the language reference. To read through the design decisions made, consult the SPICE.

Custom HTTP Headers

Pkl can now attach custom HTTP headers to outbound requests, making it possible to access private or authenticated resources (#1196, #1584).

For example, a CLI user can add an Authorization header either in settings.pkl, or the PklProject file:

  • ~/.pkl/settings.pkl

  • PklProject

amends "pkl:settings"

http {
  headers {
    ["https://my.private.server/**"] {
      ["Authorization"] = "Bearer my-secret-token"
    }
  }
}
amends "pkl:Project"

evaluatorSettings {
  http {
    headers {
      ["https://my.private.server/**"] {
        ["Authorization"] = "Bearer my-secret-token"
      }
    }
  }
}

Each key in headers is a glob pattern matched against the request URL. When a request is made, every matching pattern’s headers are added to the request.

Headers can also be configured in other ways:

  • In the pkl-gradle plugin

  • As a CLI flag

  • In the Java/Swift/Go/Kotlin evaluator APIs

  • In the pkl-executor API

Header names must conform to RFC 7230 token syntax. Certain reserved names (e.g. host, connection, content-length) and prefixes (proxy-, sec-) are forbidden.

Thank you to @kyokuping for their contributions to this feature!

To learn more, consult SPICE-0022.

Noteworthy 🎶

Resolved evaluator settings

PklProject files now resolve evaluator settings that are file paths (#1394).

Some of the settings within pkl:EvaluatorSettings represent file paths. However, these paths are resolved inconsistently:

  • Pkl is inconsistent about what relative paths mean. rootDir, moduleCacheDir, and modulePath are relative to the project dir, while ExternalReader.executable is relative to the PWD.

  • External readers defined in a PklProject have different behavior depending on the PWD.

  • The logic for resolving these paths is dependent on the caller, rather than Pkl.

For CLI users, this means that the current working directory can affect how evaluator settings are loaded.

In Pkl 0.32, these paths are resolved entirely within Pkl, and resolved against the enclosing directory.

A new method resolve() is added to pkl:EvaluatorSettings, and a new property resolvedEvaluatorSettings is added to pkl:Project. Language bindings are expected to use resolvedEvaluatorSettings when configuring the evaluator.

To read more about this change, consult SPICE-0027.

Line continuations in multiline strings

When authoring multiline strings, it is now possible to use a line continuation to break a single line’s value over multiple lines (#1507, #1564).

These two string snippets are logically identical:

str =
  """
  Although the Dodo is extinct, \
  the species will be remembered.
  """
str = "Although the Dodo is extinct, the species will be remembered."

To read more about this design, consult SPICE-0028.

Parse-time variable resolution

Pkl now resolves variables at parse time (#1429, #1622, #1634, #1706).

Currently, Pkl defers the resolution of variable names to when the node is executed. In Pkl 0.32, these are resolved when the file is parsed.

This is a pre-requisite feature for many upcoming features, such as flat member syntax and method varargs.

Additionally, this enables some runtime optimizations, including let expressions.

Let expression performance improvements

Performance improvements were made to the handling of let expressions (#1622, #1634).

Here is a sample benchmark:

amends "pkl:Benchmark"

microbenchmarks {
  ["let"] {
    expression =
      let (a = 1)
      let (b = 2)
      let (c = 3)
      let (d = 4)
      let (e = 1)
      let (f = 2)
      let (g = 3)
      let (h = 4)
      let (i = 1)
      let (j = 2)
      let (k = 3)
      let (l = 4)
      let (m = 1)
      let (n = 2)
      let (o = 3)
      let (p = 4)
      let (q = 1)
      let (r = 2)
      let (s = 3)
      let (t = 4)
        a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p + q + r + s + t
  }
}

output {
  renderer {
    converters {
      [Duration] = (it: Duration) -> super[Duration].apply(it.toUnit("ns"))
    }
  }
}

And here are the results when run on a MacBook Pro M4 Max machine: jpkl is about 12x faster, while native pkl is about 4000x faster:

Variant Results (mean)

jpkl

1208.06.ns → 97.76.ns

pkl

1275.96.ns → 0.32.ns

Test reporter

A test reporter option is added when running tests (#1563, #1597).

A new CLI flag, --test-reporter, is now available to the pkl test and pkl project package commands. This flag accepts the name of a test reporter to be used to format test result output.

There are two possible reporters:

  • spec (default) - the current reporter

  • minimal - a reporter that only emits failing tests

This option is also available to pkl-gradle users.

Dependency notation improvements

Pkl’s evaluator now accepts dependency notation URIs as source modules (#1595).

In Pkl 0.31, we introduced CLI support for dependency notation. Now, this support has been extended to the evaluator itself. This means that users of Pkl’s various language bindings can also use dependency notation.

Additionally, the limitation on local dependencies has been dropped.

pkldoc supports single-package docsites

pkldoc has improved support for single-package documentation websites (#1592).

When a docsite has only one package name, and also no overview from a docsite-info.pkl, the resulting docsite does not have a top-level package index page. Instead, it redirects to the package page, and omits the site-level element from the breadcrumb.

Allowed package imports in PklProject

PklProject files can now import package asset URIs (#1547).

This means that it is now possible to centralize and share common configurations across multiple projects.

Java Library Changes

Switch to JSpecify annotations

Pkl has switched to using JSpecify for annotating nullability (#1515, #1527, #1528, #1530, #1544, #1601, #1607).

Currently, Pkl uses annotations from the com.google.code.findbugs:jsr305 library. This suffers from some issues:

In contrast, JSpecify is widely seen as the standard for nullability annotations, and is already understood by most tools.

This impacts Pkl’s Java APIs; methods previously annotated using JSR-305 nullability annotations now use JSpecify. Additionally, the Java code generator now emits JSpecify annotations by default.

New API: org.pkl.config.java.ConfigDecoder

A new API is introduced called org.pkl.config.java.ConfigDecoder (#1533).

Currently, the org.pkl.config.java.Config interface mixes configuration representation with decoding logic. To better separate these concerns, a new API is introduced for decoding.

Example:

class Main {
  Config getConfig() {
    var decoder = new ConfigDecoderBuilder().preconfigured().build();
    return decoder.decode(bytes);
  }
}

New asNullable methods in pkl.config.java

New methods are added for decoding into nullable types (#1544).

The following methods are introduced:

  • org.pkl.config.java.Config.asNullable

  • org.pkl.config.java.JavaType.pairOfNullableFirst

  • org.pkl.config.java.JavaType.pairOfNullableSecond

  • org.pkl.config.java.JavaType.pairOfNullableFirstAndSecond

  • org.pkl.config.java.JavaType.arrayOfNullable

  • org.pkl.config.java.JavaType.iterableOfNullable

  • org.pkl.config.java.JavaType.collectionOfNullable

  • org.pkl.config.java.JavaType.listOfNullable

  • org.pkl.config.java.JavaType.setOfNullable

  • org.pkl.config.java.JavaType.mapOfNullableKeys

  • org.pkl.config.java.JavaType.mapOfNullableValues

  • org.pkl.config.java.JavaType.mapOfNullableKeysAndValues

org.pkl.config.java.Config.as can also return a null value. In a future release, this method will have a runtime non-null assertion.

pkl-formatter migrated to Java

The pkl-formatter library has been migrated from Kotlin to Java (#1514).

Therefore, its Kotlin dependency is dropped.

pkl-gradle changes

The following changes have been made to pkl-gradle.

Support for external readers

External readers can now be configured when configuring evaluators (#1578).

For example:

val myPklTask by pkl.evaluators.creating {
  val foo by externalModuleReaders.creating {
    executable = "pkl-foo-reader"
    arguments = listOf("--bar", "baz")
  }
}

Support for module/resource readers from SPI

Gradle users can now add module/resource readers by registering a service via the Java SPI mechanism (#1581).

Support for Gradle configuration cache

The plugin now supports the Gradle configuration cache feature (#1425).

Thanks to @ffluk3 for their contributions to this feature!

Breaking Changes 💔

Name resolution changes

Variables are now resolved at parse time.

As part of this change, some names are resolved differently.

Take the following code:

qux = "outer"

foo {
  when (cond) {
    qux = "inner"
  }
  res = qux
}

Currently, res = qux will eval to either res = "outer" or res = "inner" depending on whether cond evals to true or not. This is a lexical lookup that is either one level up, or zero levels up, depending on the result of code execution.

This type of resolution is not possible at parse time, because we do not know what cond executes to. In Pkl 0.32, res will always resolve to the inner qux, and this snippet will throw if the when generator does not fire.

Java API changes

The following breaking changes have been made to the Java API.

  • org.pkl.config.java.Config#makeConfig

    Removed without replacement

  • org.pkl.config.java.Config.fromPklBinary

    Deprecated, replaced with org.pkl.config.java.ConfigDecoder

  • org.pkl.config.java.mapper.NonNull

    Deprecated, replaced with org.jspecify.annotations.NonNull

  • org.pkl.core.evaluatorSettings.PklEvaluatorSettings.parse

    No longer accepts pathNormalizer argument

  • org.pkl.formatter.Formatter

    • Pass GrammarVersion to constructor instead of each format method

    • Replace format(Path): String with format(Reader, Appendable)

    • Mark as throws IOException

    • Deprecate methods that accept GrammarVersion as an argument

Loading rule changes in pkl:EvaluatorSettings

Breaking changes have been made to how evaluator settings are loaded when using PklProject (#1394).

Loading rule changes for the external reader executable

The following changes have been made for the executable property in an external reader:

  • If the executable does not contain a slash (/ on POSIX, \ on Windows) character, it is always resolved against the PATH environment variable.

  • If it does contain a slash, it is resolved relative to the enclosing PklProject directory, instead of the current working directory.

Changes to --external-module-reader and --external-resource-reader CLI flags

The --external-module-reader and --external-resource-reader CLI flags will replace any external readers otherwise configured within a PklProject, instead of add to it (#1394).

This makes this behavior consistent with how other settings work.

Bug Fixes 🐜

The following bugs have been fixed.

  • Data race in MessagePack encoder during concurrent server sends (#1486)

  • pkl-doc library publishes incorrect Maven dependency scopes (#1517)

  • Re-using pklbinary#Renderer during rendering results in incorrect output (#1525)

  • Cannot glob using subpatterns inside a project dependency (#1545)

  • Project package command incompatible with glob imports on Windows (#1556)

  • Thrown PklBugException when using power assertions with unavailable source sections (#1571)

  • pkl-doc: DocGenerator never shuts down its thread pool (#1583)

  • Imports gathering Gradle task can fail with a confusing exception (#1591)

  • HTTP rewrite fails with PklBugException under tr-TR locale when host contains 'I' (#1617)

  • Formatter bug fixes (#1619)

  • Unrelated error message thrown when computing an error message (#1629)

  • pkl-binary serialization produces an incomplete msgpack value in the presence of local properties (#1631).

  • Poor parser error message (#1638)

  • relativePathTo does not check if receiver is a module (#1649)

  • Int overflow on multiplication throws unexpected exception (#1651)

  • Incorrect eager check for Map type (#1653)

  • Incorrect toRadixString() for math.minInt (#1655)

  • Evaluator.evaluateSchema() throws if properties are annotated with @ConvertProperty (#1657)

  • String.padStart and String.padEnd return incorrect strings sometimes (#1661)

  • String.getOrNull crashes with certain strings (#1662)

  • Incorrect facts in pkl:base doc comments (#1669)

  • Index-based methods on Set aren’t implemented (#1682)

  • pkl-gradle throws exception when a Pkl task can’t create an evaluator during configuration time (#1684)

  • Unrelated exception gets thrown when validating elements/entries in objects (#1697)

  • Type argument is lost in constraint expressions within generic typealiases (#1705)

  • Typealiases with type var as root are broken (#1711)

  • Incorrect List/Set/Map/Listing/Mapping/union type check behavior through typealiases (#1710)

  • Type aliases with type var as root are broken (#1711)

  • Type checks through aliases are always run lazily even when they should be eager (#1716)

  • Improve thread safety when evaluating pkl:base (#1719)

  • Incorrect member links in doc comments (#1723)

  • pkl-gradle plugin does not allow projectpackage: reads by default (#1734)

  • IntSeq incorrectly emits on empty IntSeqs (#1738)

Security Fixes 🔒

The following security vulnerabilities were fixed: