Skip to main content

A DSL for authoring, composing, and managing LLM prompts as modular, version-controlled artifacts

Project description

PCL — Prompt Composition Language

PCL is a small DSL for authoring, composing, and managing LLM prompts as modular, version-controlled artifacts. A .pcl file compiles to a plain text string ready to use as a system prompt with any LLM API.

Why PCL?

I came up with PCL as a way to manage prompts when they get too large and unwieldy. Most agentic applications today involve persisting prompts as a bunch of text/markdown files across disparate locations which are then read at runtime before using them for an LLM inference request. Parts of the prompt are often static (rules, persona, tools) while some can be more dynamic (user messages, context). Managing multiple sets of prompt .md files, with different versions, becomes a real engineering challenge. Template engines like Jinja alleviate this problem somewhat but I wanted something that went beyond simple variable substitution and was specifically tailored for LLM applications.

With PCL you get in-built support for templating, composition, compile time checks etc. Think of prompts that same way as you do code. You define your prompts as a modular set of text files (.pcl files), then have the compiler compile and weave the prompts together into a single IR template, which can be rendered at runtime with variable substitution.

pcl files are just like any other source- version controlled, with a well defined syntax.

Author

Tanay Deshmukh (& vibe-coded with Claude)


Features

  • Blocks — define named fragments with @block name:, compose them with @include
  • Imports — split prompts across files with @import ./file.pcl [as ns]
  • Variables${var} resolved at render time; ${var | default} for fallbacks
  • Conditionals@if variable: / @if not variable: for truthiness-based branching
  • Raw blocks@raw / @end passes content through unmodified (no interpolation)
  • Comments — lines starting with # are stripped from output
  • Frontmatter — optional YAML metadata (version, description, arbitrary keys)

Installation

Requires Python 3.11+. Recommend using uv for environment management.

git clone <repo>
cd pcl
uv venv
source .venv/bin/activate
uv pip install -e ".[dev]"

Verify:

pcl --help

CLI

pcl compile

Compile a .pcl file and dump its intermediate representation — a tree of TEXT, VAR, and IF segments. Useful for inspecting template structure before rendering.

pcl compile examples/agent.pcl

Example output:

Metadata:
  version: 1.0
  description: Research assistant system prompt

Segments:
TEXT  'You are an expert research assistant...'
VAR   ${date}
IF    premium:
  TEXT  'You have access to the premium document index.'
IF    not premium:
  TEXT  'Upgrade to unlock the premium document index.'
VAR   ${query | no query provided}

Use -o/--output to write the compiled IR to a binary .pclc file instead of printing it. This enables compile-once-render-many workflows without re-parsing source files.

pcl compile examples/agent.pcl -o agent.pclc
# Compiled to agent.pclc

pcl render

Render a .pcl or .pclc file with explicit variable values.

# From source
pcl render examples/agent.pcl \
  --var date=2026-02-28 \
  --var query="What is alignment?" \
  --var premium=true

# From pre-compiled .pclc (no re-parsing)
pcl render agent.pclc \
  --var date=2026-02-28 \
  --var query="What is alignment?" \
  --var premium=true

--var accepts key=value. Values true and false are coerced to booleans.

pcl check

Validate a .pcl file without producing output. Exits 0 on success, 1 on error. Prints the error message and line number on failure.

pcl check examples/agent.pcl

pcl watch

Recompile whenever the file changes. Accepts the same --var flags as render.

pcl watch examples/agent.pcl \
  --var date=2026-02-28 \
  --var premium=false

Python API

from pcl import compile, render, serialize, deserialize, CompiledTemplate
import cbor2

# compile() — produces a CompiledTemplate (IR) with metadata and segments
template = compile("examples/agent.pcl")
print(template.metadata)    # {"version": 1.0, "description": "..."}
print(template.segments)    # [str, VarRef, Conditional, ...]

# render() — resolves variables and conditionals, returns final string
prompt = render("examples/agent.pcl", variables={
    "date": "2026-02-28",
    "query": "What is alignment?",
    "premium": True,
})
print(prompt)

# compile once, render many — avoids re-parsing for each variable set
template = compile("examples/agent.pcl")
for query in queries:
    prompt = render(template, variables={"date": "2026-02-28", "query": query})

# serialize to .pclc — write compiled IR to disk
with open("agent.pclc", "wb") as f:
    f.write(cbor2.dumps(serialize(template)))

# deserialize from .pclc — reconstruct IR without re-parsing source
with open("agent.pclc", "rb") as f:
    template = deserialize(cbor2.loads(f.read()))
prompt = render(template, variables={"date": "2026-02-28", "query": "..."})

Language Quick Reference

---
version: 1.0
description: Optional YAML frontmatter — available as metadata, not in output
---

@import ./other.pcl
@import ./lib.pcl as lib

# This is a comment — stripped from output

@block intro:
    Defined here, emitted only when @include'd.

@include intro          # emit a local block
@include lib.greet      # emit a named block from an imported file
@include other          # emit the entire body of other.pcl

@if premium:
    Shown only when premium is truthy.

@if not premium:
    Shown only when premium is falsy or absent.

Hello ${name}!              # required variable — error if missing
Hello ${name | world}!      # variable with default

@raw
${not_interpolated}  @not_a_directive  # this is not a comment
@end

\@block  →  literal @block in output
\#       →  literal # in output
\${      →  literal ${ in output

Examples

The examples/ directory contains a multi-file prompt:

File Purpose
examples/agent.pcl Main agent prompt — imports persona and tools
examples/persona.pcl Persona blocks (researcher, brief)
examples/tools.pcl Tool description blocks (search, browse, premium_index)
pcl render examples/agent.pcl \
  --var date=2026-02-28 \
  --var query="Explain quantum entanglement" \
  --var premium=false

Tests

uv run pytest tests/ -v

With coverage:

uv run pytest tests/ --cov=pcl --cov-report=term-missing

VS Code Extension

Syntax highlighting is in a separate repo: src/pcl-vscode.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pcl_lang-0.1.0.tar.gz (19.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pcl_lang-0.1.0-py3-none-any.whl (13.8 kB view details)

Uploaded Python 3

File details

Details for the file pcl_lang-0.1.0.tar.gz.

File metadata

  • Download URL: pcl_lang-0.1.0.tar.gz
  • Upload date:
  • Size: 19.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.9

File hashes

Hashes for pcl_lang-0.1.0.tar.gz
Algorithm Hash digest
SHA256 89db22384aff48bdfd20d3631f3ec580d27e6594e857908cd2b144c284ad0d95
MD5 0880756ab921e7dbb2a0b02d8df126e2
BLAKE2b-256 8f7479f64483eda02bfcfb2e973a8c20fd95192f528860bc460e4bb692d70295

See more details on using hashes here.

File details

Details for the file pcl_lang-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pcl_lang-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 13.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.9

File hashes

Hashes for pcl_lang-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5c42afb522c9e8c48516dbe28c9374949a3068528b178cbc6261652d90bb7e63
MD5 0636623447cccc2822ae538ca21de8c9
BLAKE2b-256 86ae8692822ac0ca6413e025119d4ab92853c66a0b84dca453359bce7c64ad1e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page