pkl/SKILL.md
PKL (Pickle) is Apple's open-source, programmable configuration language — a type-safe alternative to YAML/JSON/TOML. Use this skill when writing PKL configuration files, amending PKL modules, defining PKL schemas or classes, generating config output in JSON/YAML/TOML/plist, or integrating PKL into a build pipeline. Triggers on .pkl files, PKL module authoring, amend expressions, typed property definitions, Listing/Mapping usage, or pkl eval/test/project CLI commands.
npx skillsauth add mikkelkrogsholm/dev-skills pklInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
CRITICAL: Your training data for PKL is unreliable. APIs change between versions and memorized patterns may be wrong or deprecated. Before writing any code, you MUST use
WebFetchto read the live docs based on your task:
- Language syntax and types:
WebFetch("https://pkl-lang.org/main/current/language-reference/index.html")- Getting started:
WebFetch("https://pkl-lang.org/main/current/language-tutorial/01_basic_config.html")- CLI commands:
WebFetch("https://pkl-lang.org/main/current/pkl-cli/index.html")Do not proceed without fetching the relevant URL first. Never assume syntax or CLI flags — verify against current docs.
PKL (Pickle) is Apple's open-source configuration language that is programmable, type-safe, and outputs to multiple standard formats. It replaces YAML/JSON/TOML with a language that supports types, constraints, inheritance, and code reuse.
PKL goes beyond static config formats in four ways:
port: UInt16 or timeout: Duration(isPositive) are checked at evaluation time, not at runtime.amends "base.pkl" overrides selected properties while preserving others, enabling layered configs (dev/staging/prod) without duplication.output { renderer = ... } blocks.Use amends to override values; use extends only when adding new properties.
amends "base.pkl" creates a new module that changes values but cannot add new properties — the module's type stays fixed. extends "base.pkl" creates a new module subclass and allows adding members. Using extends when you only want to override values is unnecessary and breaks type guarantees if the base is not declared open.
// Correct — only changing values
amends "config/base.pkl"
port = 9090
// Only use extends when you need new properties
extends "config/base.pkl"
port = 9090
debugMode = true // new property not in base
Declare output format explicitly — PKL does not auto-select JSON or YAML.
Running pkl eval file.pkl outputs PCF (PKL's own format) by default, not JSON or YAML. To get JSON output, pass -f json on the CLI or declare the renderer inside the module. Omitting this is the most common source of "my output looks wrong" confusion.
// Inside a module — explicit renderer
output {
renderer = new JsonRenderer {}
}
// CLI equivalent
// pkl eval -f json config.pkl
// pkl eval -f yaml -o config.yaml config.pkl
Required properties have no default — they must be provided when amending.
A typed property without a default value (name: String with no = "...") is required. Amending a module that has required properties without supplying them causes a validation error. Provide defaults in base modules or always supply values in every amend chain.
// Base module with a required property
class AppConfig {
host: String // required — no default
port: UInt16 = 8080 // optional — has default
}
// Amending module MUST provide host
amends "AppConfig.pkl"
host = "localhost"
// port can be omitted — default 8080 applies
Listing and Mapping are distinct types — they do not coerce into each other.
Listing is an ordered sequence (like a JSON array); Mapping is a keyed collection (like a JSON object). PKL does not automatically convert between them. A property typed as Listing cannot receive a Mapping value, and vice versa. When amending a Listing, add elements with entries inside the amend block; do not assign a new Mapping.
// Listing — ordered, integer-indexed
tags: Listing<String> = new {
"web"
"api"
}
// Mapping — keyed
labels: Mapping<String, String> = new {
["env"] = "prod"
["team"] = "platform"
}
// Amending a Listing — add more elements
amends "base.pkl"
tags {
"extra-tag" // appended to inherited elements
}
String interpolation uses \(expression), not ${} or #{}.
PKL's interpolation syntax is a backslash followed by the expression in parentheses. Using JavaScript-style ${} or Ruby-style #{} produces a literal string instead of interpolating the value, with no error thrown.
name = "world"
greeting = "Hello, \(name)!" // correct — "Hello, world!"
wrong1 = "Hello, ${name}!" // literal string — "Hello, ${name}!"
wrong2 = "Hello, #{name}!" // literal string — "Hello, #{name}!"
Import stdlib with pkl: scheme; import local files with relative paths.
Standard library modules use the pkl: URI scheme (import "pkl:math"). Local modules use relative or absolute file URIs. HTTP imports are supported for remote modules. Forgetting the scheme or using wrong path separators causes import resolution failures.
import "pkl:math" // standard library
import "pkl:json" // JSON renderer helpers
import "./shared/types.pkl" // relative local file
import "package://pkg.pkl.tools/[email protected]#/json.pkl" // package
result = math.sqrt(16) // 4.0
# Evaluate to stdout (PCF format by default)
pkl eval config.pkl
# Evaluate to specific format
pkl eval -f json config.pkl
pkl eval -f yaml -o output.yaml config.pkl
# Evaluate multiple files, output alongside source
pkl eval -f json -o %{moduleDir}/%{moduleName}.json src/**/*.pkl
# Run tests
pkl test tests/*.pkl
pkl test --junit-reports=reports/ tests/*.pkl
# Manage project dependencies
pkl project resolve
pkl project package --output-path dist/
# Evaluate a single expression from a module
pkl eval -x metadata.version config.pkl
| Format | CLI flag | Renderer class |
|--------|----------|----------------|
| JSON | -f json | new JsonRenderer {} |
| YAML | -f yaml | new YamlRenderer {} |
| Java properties | -f properties | new PropertiesRenderer {} |
| plist | -f plist | new PListRenderer {} |
| XML/textproto | -f xml / -f textproto | new XmlRenderer {} |
development
Zod — TypeScript-first schema validation with static type inference. Use when building with Zod or asking about schema definitions, type inference, parsing, transformations, refinements, coercion, error handling, or integration with forms, APIs, or tRPC. Fetch live documentation for up-to-date details.
tools
Vite — next-generation frontend build tool with instant dev server and optimized production builds. Use when building with Vite or asking about its APIs, configuration, plugins, SSR, environment variables, or integration with frameworks. Fetch live documentation for up-to-date details.
tools
Upstash — serverless Redis, QStash, and Vector database with per-request pricing optimized for edge and serverless environments. Use when building with Upstash or asking about its Redis client, QStash message queuing, rate limiting, workflows, or vector search. Fetch live documentation for up-to-date details.
tools
Turso — edge-hosted SQLite database built on libSQL with embedded replicas, multi-tenancy, and low-latency global distribution. Use when building with Turso or asking about its libSQL client, embedded replicas, database-per-tenant patterns, auth tokens, sync, or integration with Drizzle or other ORMs. Fetch live documentation for up-to-date details.