psl

Prompt Script Language — an AI native language that lets you embed AI instructions in files written in other languages.

Wrap an instruction in :: delimiters and it becomes a slot. The psl compiler fills the slot with code the model writes, in place, in the file you already have.

Try a run

fib.go.psl
package main
import "fmt"
// :: one-line doc comment for Fib, Go style ::
func Fib(n int) int {
if n < 2 {
return n
}
a, b := 0, 1
:: fill in the iterative loop and return, using a and b ::
}
func main() {
fmt.Println(Fib(10))
}
$psl fib.go.psl

One run, one slot. Run it again until the file has none left.

The whole syntax

:: xxx ::

That is all of it. No keywords, no imports, no config in the file. Whitespace inside the delimiters is optional, and a slot may span several lines.

It writes in your language

The model is shown the whole file with the slot marked, so what comes back is written in the surrounding language and reuses the names already defined above it. A slot alone on its line is indented to that line's column.

It stays out of real code

C++'s std::cout, Rust's Foo::Bar, PHP's self::method — scope resolution always glues :: to an identifier, and a glued :: is never a delimiter. Existing code in the file keeps working the way it reads.

One slot, one run

Each run resolves the first remaining slot and writes it back into the file. The file is always a real file, never a half-generated draft, and you can stop and read it after any run.

Pick a model per slot

A bare :: xxx :: uses your default model. Name one in front of the instruction — :: gpt-5.6> xxx :: — and that slot alone goes to it.

The name says the language

A PSL file is fib.go.psl, bot.py.psl, Program.cs.psl — the extension in the middle is not decoration. C, C#, Go, JavaScript, Python, Rust, TypeScript and Macro PSL each have their own rules for which :: belong to the language; anything else compiles under the generic rules and says so.

A slot that can look things up

A slot is resolved once and frozen into the file, so an instruction that turns on a current fact is only as good as what the model remembers. Put web_search=on in a model's .pslrc section and it goes and looks — the model decides whether a slot needs a search, a second model does the searching, and every query and the pages it found go into the log.

Python at runtime

$ psl run bot.py.psl

psl run translates the PSL source into a Python file beside it, then runs that file. bot.py.psl stays untouched; bot.py is generated for Python to execute.

bot.py.psl
for i in range(3):
    print(f":: give me a new greeting for iteration {i} ::")
stdout
Hey there — first time around, and glad you came.
Round two: still here, still pleased to see you.
Third and last. Good to have you along.

Run it again and all three lines come back different.

A fresh value every time

In Python, a slot is a runtime expression. If Python reaches it three times in a loop, psl resolves it three times and returns a new value on every iteration.

Live values, full source context

The instruction is evaluated as an f-string where it runs, so it sees current loop and function values. The model also receives the complete original source and the slot's exact position.

Where a value belongs

Use a runtime slot as an expression or as the whole contents of a string. A slot mixed with other string text, or placed in a comment, is rejected; use an f-string when it needs surrounding text.

Whatever runs it, psl finds it

The second extension picks the executor as well as the rules: a virtualenv for Python, node for JavaScript, tsx, bun or deno for TypeScript, go run, a temporary binary from cc or rustc. Arguments after -- belong to your program, not to psl.

The plain psl bot.py.psl command still compiles one slot into the source on each run. Python is the first language with live runtime slots; other languages keep compile-time slot semantics under psl run — all of their slots resolved in that one run, into the generated file beside the source.

Giving a slot more to go on

--image
psl ui.tsx.psl --image design.png

Hand the slot something to look at — a screenshot, a mockup, a photo. Takes a file path, a data: URL or base64. PNG, JPEG, GIF and WebP.

--prompt
psl bot.py.psl --prompt "move(x, y) takes absolute screen pixels, origin top-left"

Tell it what the file cannot: the API being called, what each parameter means, in what units. Pass a path instead of text and psl reads the file. It is context, never the instruction.

Around the compiler

No config required

With OPENAI_API_KEY or ANTHROPIC_API_KEY exported, psl works as it stands. Write a .pslrc when you want a different default, several models in one file, or an endpoint of your own — a section is just a name, a base_url and a key, so a local model is one section like any other.

psl config

Opens the .pslrc psl would actually read here, in your editor. With no config anywhere yet it writes the example out first, so the sections are already there and only the keys are blank; a typo is reported when you close the editor, not on your next compile.

psl usage

Every request psl makes is recorded. psl usage adds it up and prints what each model has spent — requests, input, output — heaviest first. The table goes to stdout and nothing else does, so it pipes straight into whatever reads tables.

Everything it did, on the record

Every request lands as a JSON line in ~/.psl/psl.log — model, tokens, the instruction, and any pages a slot searched. Your key is never in it, and neither are image bytes; it lives outside your repositories, so there is nothing to accidentally commit.

Getting it

One line, and nothing to install alongside it. The installer takes the build for your platform from the latest release, checks it against the release's checksums — one it cannot verify is never installed — and puts psl on your PATH. No Go toolchain involved.

curl -fsSL https://raw.githubusercontent.com/lhypds/psl/main/get.sh | sh

Goes to /usr/local/bin when you can write there, and ~/.local/bin otherwise, so no root is involved either way. Pass --prefix through the pipe to put it somewhere else.

psl update

Keeps itself current from there. Every download is checked against the release's checksums — one it cannot verify is never installed, and a failed update leaves the working psl exactly where it was.

Where it is used

Pob →Desktop automation, driven by Macro PSL. The macros that operate an application are written with slots, so a step can say what to do instead of naming a coordinate.