Plugins · CCPP PQL
Functions
A PQL function is a reusable block defined in a .pql file. #id(...) expands it at compile time; FOR repeats statements.
A function is PQL you reuse: a .pql file with a YAML frontmatter (its signature) and a PQL body. A call
#id(...) produces statements at compile time. It never runs a tool or a Claude action.
---
format: 1
description: Adds numbered features.
kind: fragment
parameters:
- name: name
type: NonEmptyString
- name: n
type: integer
default: 1
minimum: 1
maximum: 20
let:
end: "params.n + 1"
---
FOR i IN RANGE(1, {{ vars.end }}) {
ADD "{{ params.name }} {{ vars.i }}";
}$ FROM @packages/pql {
#add-features("Diagnostic", 3);
}It compiles to three objectives: ADD "Diagnostic 1", "Diagnostic 2", "Diagnostic 3".
Where definitions live
| Layer | Directory |
|---|---|
| project | .claude/pql/*.pql |
| user | ~/.ccpp/pql/*.pql ($CCPP_HOME/pql/) |
The id is the file name without .pql, in kebab-case. A project definition replaces the user one entirely, even
when it is disabled or invalid. An invalid definition blocks only the programs that call it.
ccpp pql functions lists them with their status (active, disabled, overridden, invalid).
.claude/pql/ is protected by CPO like .claude/policies/: with claudeCanEdit: false, Claude cannot change a
definition.
Frontmatter
| Field | Meaning |
|---|---|
format | 1 |
description | required, at most 200 characters |
kind | fragment (default), how or program |
parameters | ordered list: name, type, optional default and constraints; required ones first |
let | variables computed with CEL from params and other vars |
id, category, enabled, claudeCanEdit, x-* | optional |
Parameter types: string, NonEmptyString, integer, number, boolean, array (with items), the
references PathRef, ExistingPathRef, ExistingDirectoryRef, NewDirectoryRef, and any identifier domain
(principle, naming-convention…). Constraints: minimum, maximum, minLength, maxLength, enum, items.
Kinds
| Kind | Called in | Produces |
|---|---|---|
fragment | a scope or branch body | statements of the active scope, without SESSION SUCCESS |
how | a body, a HOW block or another how function | HOW directives only |
program | the root of the prompt | root scopes |
Rules declared inside a fragment (stop rules, directives, policy references) apply inside it only.
Calls
#id(a, b); by position or #id(name: a, n: b); by name, never mixed. Nothing is converted: "3" is not an
integer, null is not an omitted argument, a string is not a reference. Lists are written ["a", "b"].
let, interpolation and FOR
letmaps names to CEL expressions, evaluated once per call. They readparams.*andvars.*.{{ params.x }}and{{ vars.y }}insert a value. Alone in a value position, the type is kept (LIMIT FILE TO {{ params.n }} LINES;); inside a string, a scalar is rendered as text.FOR v IN RANGE(a, b) { … }runs fromaincluded tobexcluded;FOR v IN {{ params.list }} { … }runs over a list. The variable isvars.v, read-only.FORalso works directly in a prompt.
Limits
Recursion, direct or indirect, is refused (PQL-F050). Call depth, call count, iterations, statements, output
size and CEL size are bounded for the whole compilation; going over one aborts the expansion with an error
(PQL-F060 to PQL-F065), never a truncated program. Every expanded statement remembers its file and call
chain, so a diagnostic points into the definition with via #a, #b.