Getting started — TypeScript
Install
Section titled “Install”npm install prompting-press zodpnpm add prompting-press zodyarn add prompting-press zodbun add prompting-press zodESM-only · Node 20+ · ships as a native addon (per-platform binary).
zod is a peer dependency for the typed-validation facade (the consuming project supplies its own Zod
version). A Zod schema is passed at render time; the library imports only the structural safeParse
interface, not Zod’s identity, so any object exposing safeParse works.
Write a prompt definition
Section titled “Write a prompt definition”A prompt definition is YAML, JSON, or TOML text — the same shape in any of the three formats. The
caller reads the text and hands it to the from* factories; the library does no file I/O. The
assistant definition used throughout this page:
name: assistantrole: systembody: "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words."variables: company: type: string trusted: true max_words: type: integer trusted: true{ "name": "assistant", "role": "system", "body": "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words.", "variables": { "company": { "type": "string", "trusted": true }, "max_words": { "type": "integer", "trusted": true } }}name = "assistant"role = "system"body = "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words."
[variables.company]type = "string"trusted = true
[variables.max_words]type = "integer"trusted = trueConstruct and render
Section titled “Construct and render”Step 1 — construct a Prompt (and validate)
Section titled “Step 1 — construct a Prompt (and validate)”Read the definition file and hand its text to a from* factory (fromYaml / fromJson / fromToml),
or build from a typed PromptDefinition object with new Prompt(...). The library does no file I/O —
the caller reads the file. Both routes run the same validation immediately:
import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt } from "prompting-press";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
test("construct from a file", () => { const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8")); // validates here, or throws // The same definition in JSON or TOML parses into an identical Prompt: // const assistant = Prompt.fromJson(readFileSync(defFile("assistant.json"), "utf8")); // const assistant = Prompt.fromToml(readFileSync(defFile("assistant.toml"), "utf8"));
assert.equal(assistant.name, "assistant"); assert.equal( assistant.body, "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words.", );});import assert from "node:assert/strict";import { test } from "node:test";import { Prompt, type PromptDefinition } from "prompting-press";
test("construct from an object", () => { // The constructor takes a typed PromptDefinition — an editor type-checks the // shape (field names, the role enum, each variable's `trusted` flag) at author time. const definition: PromptDefinition = { name: "assistant", role: "system", body: "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words.", variables: { company: { type: "string", trusted: true }, max_words: { type: "integer", trusted: true }, }, };
const assistant = new Prompt(definition); // same validation as the from* factories
assert.equal(assistant.name, "assistant"); // => "assistant"});Construction validates immediately: had the body referenced {{ industry }} — a variable
variables doesn’t declare — construction would have thrown a PromptRenderError, before any
render (malformed text throws LoadError). A template/variable disagreement is caught at
construction, never as a silent empty render.
Step 2 — declare the typed Vars schema
Section titled “Step 2 — declare the typed Vars schema”The render values are a caller-owned Zod schema whose keys match the prompt’s variables
(company, max_words). This schema is the render-time gate: you hand it to render in Step 3,
and its rules (here max_words must be a positive integer) decide what data is allowed to reach the
kernel. Declaring it is just the definition — the enforcement happens at render:
import assert from "node:assert/strict";import { test } from "node:test";import { z } from "zod";
// The render values are a caller-owned Zod schema. Its keys match the prompt's// `variables` (`company`, `max_words`), and `safeParse` runs before the kernel is touched.const AssistantVars = z.object({ company: z.string().min(1), max_words: z.number().int().min(1),});
test("the vars schema validates matching data", () => { const parsed = AssistantVars.safeParse({ company: "Acme Robotics", max_words: 50 }); assert.ok(parsed.success);});Step 3 — render (the schema validates the data), and read the result
Section titled “Step 3 — render (the schema validates the data), and read the result”Hand render the AssistantVars schema plus the data. The schema runs first — valid data renders;
invalid data (e.g. max_words: 0) throws PromptValidationError and the kernel is never touched. The
sample shows both the passing render and the rejection:
import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt, PromptValidationError } from "prompting-press";import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({ company: z.string().min(1), max_words: z.number().int().min(1),});
const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
test("render, and read the result", () => { const result = assistant.render(AssistantVars, { company: "Acme Robotics", max_words: 50 });
assert.equal(result.text, "You are a support assistant for Acme Robotics. Keep your replies under 50 words."); // => "You are a support assistant for Acme Robotics. Keep your replies under 50 words." assert.equal(result.variant, "default"); // => "default" (same arm assistant.body showed in Step 1) assert.match(result.templateHash, /^[0-9a-f]{64}$/); // 64-char lowercase-hex SHA-256 of the template assert.match(result.renderHash, /^[0-9a-f]{64}$/); // 64-char lowercase-hex SHA-256 of result.text assert.equal(result.guard, null); // => null (no guard requested)});
test("render validates the data through the schema — bad data is rejected before the kernel", () => { // max_words: 0 violates AssistantVars (.int().min(1)); render throws, nothing is rendered. assert.throws( () => assistant.render(AssistantVars, { company: "Acme Robotics", max_words: 0 }), PromptValidationError, );});render has two forms, and the optional variant/guard config rides in a trailing opts object:
prompt.render(schema, data, opts?); // schema form: Zod validates data first, then rendersprompt.render(data, opts?); // static form: already-typed data, marshaled directly// opts: { variant?: string; guard?: GuardConfig | null }The schema form is the one to reach for by default — it is what makes the typed-vars validation above
actually run. variant absent selects the default (root body) arm; guard absent opts out of the
advisory guard.
Complete example
Section titled “Complete example”import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt } from "prompting-press";import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({ company: z.string().min(1), max_words: z.number().int().min(1),});
test("complete example", () => { // 1. Construct from the definition file (validates here). const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
// 2 + 3. Render with the typed, Zod-validated vars. const result = assistant.render(AssistantVars, { company: "Acme Robotics", max_words: 50 }); console.log(result.text); // You are a support assistant for Acme Robotics. Keep your replies under 50 words. console.log(result.templateHash); // 64-char hex
assert.equal(result.text, "You are a support assistant for Acme Robotics. Keep your replies under 50 words."); assert.match(result.templateHash, /^[0-9a-f]{64}$/);});What RenderResult carries
Section titled “What RenderResult carries”| Field | Value |
|---|---|
text | The rendered body text. |
name | The prompt name. |
variant | The resolved variant name ("default" when none was selected). |
templateHash | SHA256(resolved variant template source) — lowercase hex. |
renderHash | SHA256(rendered output text) — lowercase hex. |
guard | `string |
Error types
Section titled “Error types”import assert from "node:assert/strict";import { readFileSync } from "node:fs";import { fileURLToPath } from "node:url";import { test } from "node:test";import { Prompt, PromptingPressError, PromptValidationError, PromptRenderError, LoadError,} from "prompting-press";import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.// Resolve the file next to this program (a real app uses its own path).const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({ company: z.string().min(1), max_words: z.number().int().min(1),});
const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
test("a rejected render surfaces a structured PromptValidationError", () => { let caught = false; try { assistant.render(AssistantVars, { company: "Acme Robotics", max_words: 0 }); } catch (err) { if (err instanceof PromptValidationError) { caught = true; for (const row of err.errors) { console.error(row.field, row.code, row.message); // "max_words" "validation" "Too small: expected number to be >=1" } assert.equal(err.errors[0]?.field, "max_words"); assert.equal(err.errors[0]?.code, "validation"); } } assert.ok(caught, "expected a PromptValidationError");
// The error hierarchy: every type extends the base, which extends Error. assert.ok(PromptValidationError.prototype instanceof PromptingPressError); assert.ok(PromptRenderError.prototype instanceof PromptingPressError); assert.ok(LoadError.prototype instanceof PromptingPressError); assert.ok(PromptingPressError.prototype instanceof Error);});The error hierarchy (all extend Error):
PromptingPressError # base; carries .errors: FieldError[]├── PromptValidationError # Zod validation failed (code "validation")├── PromptRenderError # kernel render failure (codes: unknown_variant,│ # undefined_variable, parse, render, excluded_feature)└── LoadError # malformed YAML/JSON/TOML (code "load")Each FieldError carries field, code, and message. A rejected render value is never
echoed onto the error surface; a parse error carries template-syntax detail (no bound values).
Next steps
Section titled “Next steps”- Variants — declare alternative bodies and select one at render.
- Deriving a prompt — produce a changed copy of an immutable prompt.
- Metadata — attach and read back opaque prompt-level and per-variant metadata.
- Compose multi-message prompts — aggregate
Promptobjects into a role-tagged sequence. - The advisory guard — tag untrusted variables and surface the advisory guard text.
- Lint prompts in CI — wire
prompt.check()as a test that fails the build. - Template features — which MiniJinja features are supported (and which are deliberately excluded).
- TypeScript API reference — full method signatures.
docs current as of 0.2.0