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 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:
-
It leaves users vulnerable to syntax errors in this system.
-
It’s hard to validate these beyond that they are a string.
-
Pkl has no understanding of the relationship between
taskAandtaskB.
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 toMyReference<String>. -
The domain must line up.
ref.Reference<MyDomain, Int>is not assignable toref.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, andmodulePathare relative to the project dir, whileExternalReader.executableis 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
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 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:
-
The JSR-305 proposal itself is dormant and unlikely to be adopted.
-
The
com.google.code.findbugs:jsr305library contributes packages tojavax, which violates the Oracle Binary Code License Agreement.
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.
Standard Library Changes
pkl:EvaluatorSettings
-
New method:
EvaluatorSettings.resolve() -
New method:
EvaluatorSettings.resolveForOs() -
New property:
EvaluatorSettings#ExternalReader.workingDir -
New property:
EvaluatorSettings#Http.headers -
New typealias:
EvaluatorSettings.HttpHeaders -
New typealias:
EvaluatorSettings.HttpHeaderName -
New typealias:
EvaluatorSettings.HttpHeaderValue
pkl:base
-
New property:
String.isGlobPattern
pkl:Project
-
New property:
Project.resolvedEvaluatorSettings
pkl:ref
-
New module introduced.
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#makeConfigRemoved without replacement
-
org.pkl.config.java.Config.fromPklBinaryDeprecated, replaced with
org.pkl.config.java.ConfigDecoder -
org.pkl.config.java.mapper.NonNullDeprecated, replaced with
org.jspecify.annotations.NonNull -
org.pkl.core.evaluatorSettings.PklEvaluatorSettings.parseNo longer accepts
pathNormalizerargument -
org.pkl.formatter.Formatter-
Pass
GrammarVersionto constructor instead of eachformatmethod -
Replace
format(Path): Stringwithformat(Reader, Appendable) -
Mark as
throws IOException -
Deprecate methods that accept
GrammarVersionas 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 thePATHenvironment 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)
-
Intoverflow on multiplication throws unexpected exception (#1651) -
Incorrect eager check for
Maptype (#1653) -
Incorrect
toRadixString()formath.minInt(#1655) -
Evaluator.evaluateSchema()throws if properties are annotated with@ConvertProperty(#1657) -
String.padStartandString.padEndreturn incorrect strings sometimes (#1661) -
String.getOrNullcrashes with certain strings (#1662) -
Incorrect facts in
pkl:basedoc 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) -
IntSeqincorrectly emits on emptyIntSeqs (#1738)
Security Fixes 🔒
The following security vulnerabilities were fixed:
-
Remote packages can read local filesystem items by escaping local dependency roots (GHSA-fgvf-hh2w-cxff, #1737)
-
Packages can be read/written outside the configured cache directory (GHSA-87qh-25w9-mh34, #1683)
Contributors 🙏
We would like to thank the contributors to this release (in alphabetical order):