# Schema Guide

Human-readable guide to PromptPack entities and their properties. For the auto-generated technical reference, see [Schema Reference](./schema-reference).

## Root Properties

The root object of every PromptPack file. Required fields are `id`, `name`, `version`, `template_engine`, and `prompts`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `$schema` | string | No | JSON Schema reference for validation and IDE support. Default: `https://promptpack.org/schema/latest/promptpack.schema.json` |
| `id` | string | **Yes** | Unique identifier for the pack. Lowercase with hyphens only. Pattern: `^[a-z][a-z0-9-]*$` (1--100 chars). |
| `name` | string | **Yes** | Human-readable display name for the pack (1--200 chars). |
| `version` | string | **Yes** | Pack version following [Semantic Versioning 2.0.0](https://semver.org/). Optional `v` prefix. Examples: `"1.0.0"`, `"v2.1.3"`. |
| `description` | string | No | Detailed description of the pack's purpose. Supports markdown (max 5000 chars). |
| `template_engine` | [TemplateEngine](#template-engine) | **Yes** | Template engine configuration shared across all prompts. |
| `prompts` | object&lt;string, [Prompt](#prompt)&gt; | **Yes** | Map of task type keys to prompt definitions. Must contain at least one entry. |
| `fragments` | object&lt;string, string&gt; | No | Shared template fragments. Keys are fragment names, values are fragment content strings. |
| `tools` | object&lt;string, [Tool](#tool)&gt; | No | Tool definitions that prompts can reference. Keys are tool names, values are tool specs. |
| `metadata` | [Metadata](#metadata) | No | Optional pack-level metadata for categorization and discovery. |
| `compilation` | [Compilation](#compilation) | No | Information about when and how the pack was compiled. Generated by `packc`. |
| `evals` | [Eval](#eval)[] | No | Pack-level eval definitions. Cross-cutting quality checks that apply to all prompts. *(v1.2+)* |
| `workflow` | [WorkflowConfig](#workflowconfig) | No | State-machine workflow over the pack's prompts with event-driven transitions. *(v1.3+)* |
| `agents` | [AgentsConfig](#agentsconfig) | No | Agent configuration mapping prompts to A2A-compatible agent definitions. *(v1.3+)* |
| `skills` | [SkillSource](#skillsource)[] | No | Skill sources for progressive-disclosure knowledge loading. *(v1.3.1+)* |
| `requires` | [Requires](#provider-requirements-v151) | No | External resources the pack needs to run. Currently carries `providers` (logical model-provider requirements). *(v1.5.1+)* |

:::note[Collections are keyed maps, not arrays]
`prompts`, `fragments`, and `tools` are all **objects** (keyed maps), not arrays. Each key serves as the identifier for the entry. For example, `prompts` maps task type strings like `"support"` or `"billing"` to their Prompt definitions.
:::

### Minimal Example

```json
{
  "$schema": "https://promptpack.org/schema/latest/promptpack.schema.json",
  "id": "my-pack",
  "name": "My Pack",
  "version": "1.0.0",
  "template_engine": {
    "version": "v1",
    "syntax": "{{variable}}"
  },
  "prompts": {
    "greeting": {
      "id": "greeting",
      "name": "Greeter",
      "version": "1.0.0",
      "system_template": "You are a friendly assistant for {{company}}."
    }
  }
}
```

---

## Template Engine

Configuration for how variables are substituted and fragments are resolved across all prompts.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | string | **Yes** | Template engine version (e.g., `"v1"`). |
| `syntax` | string | **Yes** | Variable substitution syntax pattern (e.g., `"{{variable}}"`). |
| `features` | string[] | No | Supported template features. Allowed values: `"basic_substitution"`, `"fragments"`, `"conditionals"`, `"loops"`, `"filters"`. |

```json
"template_engine": {
  "version": "v1",
  "syntax": "{{variable}}",
  "features": ["basic_substitution", "fragments"]
}
```

---

## Prompt

A single prompt configuration within a pack. Each prompt represents a specific task type (e.g., `"support"`, `"sales"`) with its own template, variables, tools, and validation rules. Prompts can evolve independently with their own version numbers.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | **Yes** | Unique identifier, typically matching the map key. Pattern: `^[a-z][a-z0-9_-]*$`. |
| `name` | string | **Yes** | Human-readable name. |
| `version` | string | **Yes** | Prompt version following Semantic Versioning, independent from the pack version. |
| `system_template` | string | **Yes** | The system prompt template. Use template syntax (e.g., `{{variable}}`) for variable substitution. |
| `description` | string | No | Detailed description of the prompt's purpose and behavior. |
| `variables` | [Variable](#variable)[] | No | Variable definitions for template placeholders. |
| `tools` | string[] | No | List of tool names this prompt can use. Tools must be defined in the pack-level `tools` map. |
| `tool_policy` | [ToolPolicy](#tool-policy) | No | Policy governing how tools can be used. |
| `pipeline` | [PipelineConfig](#pipeline-config) | No | Pipeline configuration defining processing stages and middleware. |
| `parameters` | [Parameters](#parameters) | No | LLM generation parameters (temperature, max_tokens, etc.). |
| `provider` | string | No | Key of the [provider requirement](#provider-references-v190) that runs this prompt. Absent means `default`. *(v1.9.0+)* |
| `validators` | [Validator](#validator)[] | No | Validation rules (guardrails) applied to LLM responses. |
| `tested_models` | [TestedModel](#tested-model)[] | No | Model testing results documenting performance across different models. |
| `model_overrides` | object&lt;string, [ModelOverride](#model-override)&gt; | No | Model-specific template modifications. Keys are model names (e.g., `"claude-3-opus"`, `"gpt-4"`). |
| `evals` | [Eval](#eval)[] | No | Prompt-level eval definitions. Override pack-level evals by `id`. *(v1.2+)* |
| `media` | [MediaConfig](#media-config) | No | Multimodal content configuration. Defines supported media types and constraints. *(v1.1+)* |
| `extensions` | object | No | Opaque policy annotations about this object. Never interpreted by this specification, and never passed to a scorer, guardrail or model as configuration. Keys SHOULD be namespaced. See [Policy annotation](#policy-annotation-v180). *(v1.8+)* |

```json
"prompts": {
  "support": {
    "id": "support",
    "name": "Support Bot",
    "version": "1.0.0",
    "system_template": "You are a {{role}} assistant for {{company}}.\n\nHelp resolve their issue.",
    "variables": [
      { "name": "role", "type": "string", "required": true, "example": "support agent" },
      { "name": "company", "type": "string", "required": true, "default": "TechCo" }
    ],
    "tools": ["lookup_order", "create_ticket"],
    "parameters": { "temperature": 0.7, "max_tokens": 1500 }
  }
}
```

---

## Variable

A template variable definition with type information and validation rules.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | **Yes** | Variable name used in templates (e.g., `{{name}}`). Pattern: `^[a-zA-Z_][a-zA-Z0-9_]*$`. |
| `type` | string | **Yes** | Data type. Schema is open — common values are `"string"`, `"number"`, `"boolean"`, `"object"`, `"array"`. |
| `required` | boolean | **Yes** | Whether this variable must be provided at runtime. |
| `default` | any | No | Default value when the variable is not provided. Should not be set when `required: true`. |
| `description` | string | No | Human-readable description of the variable's purpose. |
| `example` | any | No | Example value showing expected format and content. |
| `validation` | [Validation](#validation) | No | Validation rules applied to the variable value at runtime. |
| `binding` | [Binding](#binding) | No | Declares how this variable is automatically populated from runtime context (e.g., session, env, request header) without caller input. |

### Validation

Rules applied to variable values at runtime.

| Field | Type | Description |
|-------|------|-------------|
| `pattern` | string | Regular expression pattern (for string types). |
| `min_length` | integer | Minimum string length. Minimum value: 0. |
| `max_length` | integer | Maximum string length. Minimum value: 1. |
| `minimum` | number | Minimum numeric value (for number types). |
| `maximum` | number | Maximum numeric value (for number types). |
| `enum` | any[] | List of allowed values. |

### Binding

Declares how a variable is auto-populated from runtime context, so the caller doesn't have to supply it explicitly.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `kind` | string | No | The binding source kind. Schema is open — common values are `"context"`, `"session"`, `"env"`, `"header"`. |
| `field` | string | No | The field name within the binding source to extract (e.g., `"user_id"`, `"locale"`). |
| `auto_populate` | boolean | No | Whether this variable is automatically populated at runtime without caller input. Default: `false`. |
| `filter` | string | No | Optional filter expression applied to the bound value (e.g., `"lowercase"`, `"trim"`). |

```json
{
  "name": "user_id",
  "type": "string",
  "required": true,
  "binding": {
    "kind": "session",
    "field": "user_id",
    "auto_populate": true
  }
}
```

```json
{
  "name": "priority",
  "type": "string",
  "required": true,
  "description": "Support ticket priority level",
  "validation": {
    "enum": ["low", "medium", "high", "urgent"]
  }
}
```

---

## Tool

A tool definition following the function calling convention. Tools enable the LLM to call external functions. Tools are defined at the pack level and referenced by name in each prompt's `tools` array.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | **Yes** | Tool name. Pattern: `^[a-zA-Z_][a-zA-Z0-9_]*$`. |
| `description` | string | **Yes** | What the tool does. The LLM uses this to decide when to call it. |
| `parameters` | object | No | JSON Schema object defining the tool's input parameters (see below). |

### Tool Parameters (JSON Schema Format)

Tool parameters use standard JSON Schema format. The `parameters` object must have `type: "object"` with a `properties` map and an optional `required` array.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | **Yes** | Must be `"object"`. |
| `properties` | object | **Yes** | Map of parameter names to their JSON Schema definitions. |
| `required` | string[] | No | List of required parameter names. |

```json
"tools": {
  "create_ticket": {
    "name": "create_ticket",
    "description": "Create a support ticket with title, description, and priority",
    "parameters": {
      "type": "object",
      "properties": {
        "title": {
          "type": "string",
          "description": "Ticket title"
        },
        "priority": {
          "type": "string",
          "enum": ["low", "medium", "high", "urgent"]
        }
      },
      "required": ["title"]
    }
  }
}
```

---

## Tool Policy

Governance policy for tool usage. Controls when and how tools can be called by the LLM.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `tool_choice` | string | No | `"auto"` (LLM decides), `"required"` (must use tools), or `"none"` (tools disabled). Default: `"auto"`. |
| `max_rounds` | integer | No | Maximum number of LLM-tool-LLM cycles per turn. Minimum: 1. Default: 5. |
| `max_tool_calls_per_turn` | integer | No | Maximum tool calls allowed in a single turn. Minimum: 1. Default: 10. |
| `blocklist` | string[] | No | Tool names that are not allowed for this prompt (overrides the `tools` list). |

---

## Parameters

LLM generation parameters controlling the model's behavior and output characteristics.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `temperature` | number | No | Sampling temperature (0--2). Higher values increase randomness. |
| `max_tokens` | integer | No | Maximum number of tokens to generate. Minimum: 1. |
| `top_p` | number | No | Nucleus sampling parameter (0--1). Alternative to temperature. |
| `top_k` | integer or null | No | Top-k sampling. `null` means no limit. Minimum: 1. |
| `frequency_penalty` | number | No | Penalty for token frequency (-2 to 2). Positive values reduce repetition. |
| `presence_penalty` | number | No | Penalty for token presence (-2 to 2). Positive values encourage new topics. |

```json
"parameters": {
  "temperature": 0.7,
  "max_tokens": 1500,
  "top_p": 0.9
}
```

---

## Validator

A validation rule (guardrail) applied to LLM responses.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | No | Identifier, so a governance obligation control can name this validator. Lowercase letters, digits, `_` and `-`; unique across the pack where declared. Does not change how the validator runs. *(v1.8+)* |
| `type` | string | **Yes** | The validator type that determines how validation is performed. Not an enum — runtimes define and register their own types. Examples: `"banned_words"`, `"max_length"`, `"length"`, `"max_sentences"`, `"regex_match"`, `"sentiment"`, `"custom"`. |
| `enabled` | boolean | No | Whether this validator is active. Default: `true`. This is the supported way to turn a validator off. |
| `fail_on_violation` | boolean | No | **Deprecated in v1.7.0, removed in v2.0.0.** Ignored — validators always enforce. See below. |
| `message` | string | No | User-facing message returned when the validator blocks content (e.g., `"Response contains banned words"`). |
| `params` | object | No | Validator-specific parameters (e.g., word lists, character limits). |
| `extensions` | object | No | Opaque policy annotations about this object. Never interpreted by this specification, and never passed to a scorer, guardrail or model as configuration. Keys SHOULD be namespaced. See [Policy annotation](#policy-annotation-v180). *(v1.8+)* |

```json
"validators": [
  {
    "type": "banned_words",
    "enabled": true,
    "params": {
      "words": ["impossible", "can't help"]
    }
  },
  {
    "type": "pii_detection",
    "enabled": true
  }
]
```

### `fail_on_violation` is deprecated *(v1.7+)*

Declaring a validator means it enforces. A triggered validator rewrites or blocks the assistant message, and `fail_on_violation` is no longer consulted (RFC 0015). The property remains schema-valid through the v1.x series and is removed in v2.0.0.

The old `fail_on_violation: false` conflated two different things. A **guardrail** changes the message on a hit and belongs in production; an **eval** records a score and belongs in tests. A validator that fired and did nothing was an eval in the wrong place — and worse, it made a security-shaped feature fail open, because a reader reasonably assumes a validator with `enabled: true` validates.

Migrating depends on which value you used:

| You had | Do this |
|---|---|
| `fail_on_violation: true`, or the property absent | Nothing. Delete the line when convenient — the validator already enforced. |
| `fail_on_violation: false` | Move the rule to `evals` and assert on the score in your test scenarios. |

```json
// Before — a guardrail that never fired
"validators": [
  { "type": "length_check", "fail_on_violation": false, "params": { "max_characters": 1000 } }
]

// After — measurement declared as measurement
"evals": [
  {
    "id": "response_length",
    "type": "length_check",
    "trigger": "on_response",
    "params": { "max_characters": 1000 },
    "metric": "within_limit",
    "threshold": 0.95
  }
]
```

To disable a validator entirely, use `enabled: false`. That is honest about doing nothing, which `fail_on_violation: false` was not.

---

## Tested Model

Documents which models have been tested with a prompt and their performance metrics.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `provider` | string | **Yes** | LLM provider name (e.g., `"openai"`, `"anthropic"`). |
| `model` | string | **Yes** | Specific model identifier (e.g., `"gpt-4"`, `"claude-3-opus"`). |
| `date` | string | **Yes** | Date tested in `YYYY-MM-DD` format. |
| `success_rate` | number | No | Success rate from test runs (0--1). |
| `avg_tokens` | number | No | Average tokens used per response. |
| `avg_cost` | number | No | Average cost per execution in USD. |
| `avg_latency_ms` | number | No | Average response latency in milliseconds. |
| `notes` | string | No | Additional observations about model performance. |

---

## Model Override

Model-specific template modifications. Allows customizing prompts for specific models without changing the base template.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `system_template_prefix` | string | No | Text prepended to the system template for this model. |
| `system_template_suffix` | string | No | Text appended to the system template for this model. |
| `system_template` | string | No | Complete replacement system template (overrides the base template entirely). |
| `parameters` | [Parameters](#parameters) | No | Model-specific parameter overrides. |

```json
"model_overrides": {
  "claude-3-opus": {
    "system_template_prefix": "<thinking>\n",
    "parameters": { "temperature": 0.5 }
  }
}
```

---

## Pipeline Config

Pipeline configuration defining processing stages and middleware.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `stages` | string[] | **Yes** | Ordered list of pipeline stages (e.g., `["template", "provider", "validator"]`). |
| `middleware` | [MiddlewareConfig](#middleware-config)[] | No | Middleware components applied in order. |

### Middleware Config

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | **Yes** | Middleware type identifier (e.g., `"template"`, `"provider"`, `"validator"`). |
| `config` | object | No | Type-specific configuration. |

---

## Metadata

Optional pack-level metadata for categorization and discovery.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `domain` | string | No | Domain or category (e.g., `"customer-service"`, `"healthcare"`). |
| `language` | string | No | Primary language code (ISO 639-1, e.g., `"en"`). Pattern: `^[a-z]{2}$`. |
| `tags` | string[] | No | Tags for categorization and discovery. |
| `cost_estimate` | object | No | Cost estimation with `min_cost_usd`, `max_cost_usd`, and `avg_cost_usd`. |

---

## Compilation

Compiler-generated information about when and how the pack was built.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `compiled_with` | string | **Yes** | Version of the packc compiler (e.g., `"packc-v0.1.0"`). |
| `created_at` | string | **Yes** | ISO 8601 timestamp when the pack was compiled. |
| `schema` | string | **Yes** | Pack format schema version (e.g., `"v1"`). |
| `source` | string | No | Source configuration file path. |

---

## Evals *(v1.2+)*

Evals are automated quality checks on LLM outputs. Unlike validators (which run inline and can block responses), evals run asynchronously and produce scores or metrics. Evals can be defined at both pack level (cross-cutting) and prompt level (prompt-specific). Prompt-level evals with the same `id` override pack-level evals.

### Eval

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | **Yes** | Unique identifier for this eval within its scope. |
| `type` | string | **Yes** | Assertion type determining how the eval runs. Not an enum — runtimes register their own types (e.g., `"contains"`, `"regex"`, `"json_valid"`, `"llm_judge"`, `"cosine_similarity"`). |
| `trigger` | string | **Yes** | When this eval fires. Schema is open — common values are `"every_turn"`, `"on_session_complete"`, `"on_conversation_complete"`, `"on_workflow_step"`, `"sample_turns"`, `"sample_sessions"`. |
| `description` | string | No | Human-readable description of what this eval measures. |
| `enabled` | boolean | No | Whether this eval is active. Default: `true`. |
| `sample_percentage` | number | No | Percentage of turns/sessions to sample (0–100). Only used with `sample_turns` and `sample_sessions` triggers. Default: `5`. |
| `params` | object | No | Type-specific configuration. Structure depends on the eval `type`. |
| `metric` | [MetricDef](#metric-def) | No | Prometheus-style metric declaration for exposing eval results. |
| `threshold` | [Threshold](#threshold) | No | Pass/fail threshold for the eval score. |
| `message` | string | No | Human-readable message describing the eval result or failure reason. |
| `when` | object | No | Conditional expression that determines whether this eval runs for a given turn or session (e.g., `{ "has_variable": "customer_tier" }`, `{ "turn_count_gte": 3 }`). Free-form — runtimes interpret. |
| `groups` | string[] | No | Eval group tags for organizing and filtering evals (e.g., `["quality", "tone"]`, `["safety", "compliance"]`). |
| `extensions` | object | No | Opaque policy annotations about this object. Never interpreted by this specification, and never passed to a scorer, guardrail or model as configuration. Keys SHOULD be namespaced. See [Policy annotation](#policy-annotation-v180). *(v1.8+)* |

### Metric Def

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | **Yes** | Metric name following Prometheus conventions (`snake_case`). Pattern: `^[a-zA-Z_:][a-zA-Z0-9_:]*$`. |
| `type` | string | **Yes** | Metric type. One of: `"gauge"`, `"counter"`, `"histogram"`, `"boolean"`. |
| `range` | object | No | Optional value bounds with `min` and/or `max` fields. |

The `metric` object uses `additionalProperties: true`, so runtimes can attach extra fields (e.g., `labels`, `help`, `buckets`).

### Threshold

Optional pass/fail threshold attached to an eval. Runtimes compare the eval's numeric score against `value` using `operator`.

| Field | Type | Description |
|-------|------|-------------|
| `operator` | string | Comparison operator. Common values: `"gte"`, `"lte"`, `"gt"`, `"lt"`, `"eq"`. |
| `value` | number | The threshold value to compare against (e.g., `0.8`, `4`, `0.95`). |

```json
"evals": [
  {
    "id": "json_format",
    "type": "json_valid",
    "trigger": "every_turn",
    "description": "Verify the assistant always returns valid JSON",
    "metric": {
      "name": "promptpack_json_valid",
      "type": "boolean"
    }
  },
  {
    "id": "tone-check",
    "type": "llm_judge",
    "trigger": "sample_turns",
    "sample_percentage": 10,
    "params": {
      "judge_prompt": "Rate the response tone on a 1-5 scale for professionalism.",
      "model": "gpt-4o",
      "passing_score": 4
    },
    "metric": {
      "name": "promptpack_tone_score",
      "type": "gauge",
      "range": { "min": 1, "max": 5 }
    }
  }
]
```

:::note[Validators vs Evals]
Both sit on the quality spectrum: **validators** run inline on every response and can block output (`fail_on_violation`), while **evals** run asynchronously and produce scores/metrics without blocking. Use validators for hard guardrails, evals for quality measurement and monitoring.
:::

---

## Fragments

Fragments are shared, reusable template text blocks defined at the pack level. They are simple string values keyed by name.

```json
"fragments": {
  "customer_context": "Customer: {{customer_name}}\nAccount Type: {{account_type}}",
  "greeting": "Hello! How can I help you today?",
  "escalation_notice": "I'm going to connect you with a specialist."
}
```

Prompts reference fragments in their `system_template` using template syntax: `{{fragments.customer_context}}`.

---

## Multimodal Support *(v1.1+)*

PromptPack v1.1 adds multimodal content support through media configuration on individual prompts.

### Media Config

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `enabled` | boolean | **Yes** | Whether multimodal content is enabled for this prompt. |
| `supported_types` | string[] | No | Supported media types (e.g., `"image"`, `"audio"`, `"video"`, `"document"`). Custom types allowed. |
| `image` | [ImageConfig](#image-config) | No | Image-specific constraints. |
| `audio` | [AudioConfig](#audio-config) | No | Audio-specific constraints. |
| `video` | [VideoConfig](#video-config) | No | Video-specific constraints. |
| `document` | [DocumentConfig](#document-config) | No | Document-specific constraints. |
| `examples` | [MultimodalExample](#multimodal-example)[] | No | Example multimodal messages. |

### Image Config

| Field | Type | Description |
|-------|------|-------------|
| `max_size_mb` | integer | Maximum file size in MB. |
| `allowed_formats` | string[] | Allowed formats: `"jpeg"`, `"jpg"`, `"png"`, `"webp"`, `"gif"`, `"bmp"`. |
| `default_detail` | string | Detail level: `"low"`, `"high"`, or `"auto"` (default: `"auto"`). |
| `require_caption` | boolean | Whether captions are required (default: false). |
| `max_images_per_msg` | integer | Maximum images per message. |

### Audio Config

| Field | Type | Description |
|-------|------|-------------|
| `max_size_mb` | integer | Maximum file size in MB. |
| `allowed_formats` | string[] | List of allowed audio format strings (open list, e.g. `"mp3"`, `"wav"`, `"ogg"`, `"webm"`). |
| `max_duration_sec` | integer | Maximum duration in seconds. |
| `require_metadata` | boolean | Whether metadata is required (default: false). |

### Video Config

| Field | Type | Description |
|-------|------|-------------|
| `max_size_mb` | integer | Maximum file size in MB. |
| `allowed_formats` | string[] | Allowed formats: `"mp4"`, `"webm"`, `"mov"`, `"avi"`, `"mkv"`. |
| `max_duration_sec` | integer | Maximum duration in seconds. |
| `require_metadata` | boolean | Whether metadata is required (default: false). |

### Document Config

| Field | Type | Description |
|-------|------|-------------|
| `max_size_mb` | integer | Maximum file size in MB. |
| `allowed_formats` | string[] | Allowed formats (e.g., `"pdf"`, `"docx"`, `"csv"`). |
| `max_pages` | integer | Maximum pages/sheets. |
| `require_metadata` | boolean | Whether metadata is required (default: false). |
| `extraction_mode` | string | Content extraction: `"text"`, `"structured"`, or `"raw"` (default: `"text"`). |

### Generic Media Type Config

For custom media types not covered by the specific configs above.

| Field | Type | Description |
|-------|------|-------------|
| `max_size_mb` | integer | Maximum file size in MB. |
| `allowed_formats` | string[] | Allowed file formats/extensions. |
| `require_metadata` | boolean | Whether metadata is required (default: false). |
| `validation_params` | object | Custom validation parameters specific to this media type. |

### Multimodal Example

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | **Yes** | Name identifying this example. |
| `description` | string | No | What this example demonstrates. |
| `role` | string | **Yes** | Message role: `"user"`, `"assistant"`, or `"system"`. |
| `parts` | [ContentPart](#content-part)[] | **Yes** | Message content parts (text and/or media). |

### Content Part

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | **Yes** | Part type (e.g., `"text"`, `"image"`, `"audio"`). |
| `text` | string | No | Text content (when type is `"text"`). |
| `media` | [MediaReference](#media-reference) | No | Media reference (when type is a media type). |

### Media Reference

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `mime_type` | string | **Yes** | MIME type (e.g., `"image/jpeg"`, `"audio/mp3"`). |
| `file_path` | string | No | Path to media file. |
| `url` | string | No | URL to media file. |
| `base64` | string | No | Base64-encoded media data. |
| `detail` | string | No | Detail level for images: `"low"`, `"high"`, or `"auto"`. |
| `caption` | string | No | Caption or description for the media. |

```json
"media": {
  "enabled": true,
  "supported_types": ["image"],
  "image": {
    "max_size_mb": 20,
    "allowed_formats": ["jpeg", "png", "webp"],
    "default_detail": "high",
    "max_images_per_msg": 5
  }
}
```

---

## Workflow *(v1.3+)*

PromptPack v1.3 adds a state-machine workflow over the pack's prompts. Each state references a prompt key and declares event-driven transitions to other states.

### WorkflowConfig

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | integer | **Yes** | Workflow schema version. Use `1` for the current stable format. |
| `entry` | string | **Yes** | Name of the initial state. Must match a key in the `states` object. |
| `states` | object&lt;string, [WorkflowState](#workflowstate)&gt; | **Yes** | Map of state name to state definition. Must contain at least one entry. |
| `engine` | object | No | Optional runtime engine configuration. Hosts the standardized [`budget`](#workflowbudget) field *(v1.4+)* alongside runtime-specific hints (timeout, concurrency, etc.). |

### WorkflowState

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `prompt_task` | string | **Yes** | Reference to a prompt key defined in the pack's `prompts` object. |
| `description` | string | No | Human-readable description of this state's purpose. |
| `on_event` | object&lt;string, string&gt; | No | Map of event name to target state name. When the named event fires, the workflow transitions to the target state. Terminal states should omit it (or set it to `{}`). |
| `persistence` | string | No | Whether conversation context is kept (`persistent`) or reset (`transient`) on entry. |
| `orchestration` | string | No | How this state is orchestrated: `internal` (runtime manages), `external` (caller manages), or `hybrid`. Declares who *initiates* a transition — distinct from `control`. |
| `control` | string | No | Who holds the next turn after entering this state: `user` (default — yield to the user) or `agent` (run another agent round without yielding, for transient routing or processing states). Inert on states reached via `external` orchestration. *(v1.7+)* |
| `skills` | string | No | Skill filter for this state. A path scoping which skills are available, or `"none"` to disable skills. *(v1.3.1+)* |
| `terminal` | boolean | No | If `true`, this state is a terminal state — the workflow completes after its prompt executes. Terminal states should not declare `on_event` transitions. Default: `false`. *(v1.4+)* |
| `max_visits` | integer | No | Maximum number of times this state may be entered during a single workflow execution. Minimum: 1. When the limit is reached the workflow transitions to `on_max_visits`, or terminates with a budget-exhausted status if `on_max_visits` is not set. *(v1.4+)* |
| `on_max_visits` | string | No | Target state to transition to when `max_visits` is reached. Must reference a key in the `states` object. *(v1.4+)* |
| `artifacts` | object&lt;string, [ArtifactDef](#artifactdef)&gt; | No | Named artifact slots for lightweight, structured metadata that flows across state visits. Values are exposed to the prompt as `{{artifacts.<name>}}`. *(v1.4+)* |
| `extensions` | object | No | Opaque policy annotations about this object. Never interpreted by this specification, and never passed to a scorer, guardrail or model as configuration. Keys SHOULD be namespaced. See [Policy annotation](#policy-annotation-v180). *(v1.8+)* |

```json
"workflow": {
  "version": 1,
  "entry": "triage",
  "states": {
    "triage": {
      "prompt_task": "triage",
      "description": "Route incoming requests to the appropriate specialist",
      "on_event": {
        "billing": "billing_support",
        "technical": "tech_support",
        "resolved": "closing"
      }
    },
    "billing_support": {
      "prompt_task": "billing",
      "on_event": { "resolved": "closing", "escalate": "human_handoff" },
      "persistence": "persistent"
    },
    "tech_support": {
      "prompt_task": "technical",
      "on_event": { "resolved": "closing", "escalate": "human_handoff" },
      "persistence": "persistent"
    },
    "closing": {
      "prompt_task": "closing",
      "on_event": {},
      "orchestration": "internal"
    },
    "human_handoff": {
      "prompt_task": "handoff",
      "on_event": {},
      "orchestration": "external"
    }
  }
}
```

### ArtifactDef *(v1.4+)*

Declares one named slot in a state's `artifacts` map. Artifacts carry lightweight, structured metadata across state visits — pointers (commit SHAs, file paths, URIs), compact representations (schemas, summaries, diffs), or small structured results. They are not bulk data. Values persist across loop iterations and are accessible to prompts as `{{artifacts.<name>}}`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | **Yes** | MIME type indicating the artifact's content type (e.g., `"text/plain"`, `"application/json"`, `"text/markdown"`, `"text/x-python"`). Used by runtimes to determine serialization and presentation. |
| `description` | string | No | Human-readable description of what this artifact contains and how it's used. |
| `mode` | string | No | How the artifact is updated across visits. `"replace"` (default) overwrites the previous value on each visit; `"append"` accumulates content across visits (e.g., a log). |

```json
"artifacts": {
  "commit_sha":  { "type": "text/plain",       "description": "Latest generated commit" },
  "test_report": { "type": "application/json", "description": "Test runner summary" },
  "iteration_log": {
    "type": "text/plain",
    "mode": "append",
    "description": "Per-visit log accumulated across the loop"
  }
}
```

### WorkflowBudget *(v1.4+)*

Resource budget for an entire workflow execution. Provides a global safety net independent of per-state `max_visits` — when any limit is reached the workflow terminates with a budget-exhausted status. All fields are optional; omitting a field means no limit for that resource. The budget lives at `workflow.engine.budget`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `max_total_visits` | integer | No | Maximum total state visits across all states in the workflow. Minimum: 1. |
| `max_tool_calls` | integer | No | Maximum total tool calls across all states. Minimum: 1. |
| `max_wall_time_sec` | integer | No | Maximum wall-clock time in seconds for the entire workflow execution. Minimum: 1. |

```json
"workflow": {
  "version": 1,
  "entry": "plan",
  "states": {
    "plan":      { "prompt_task": "plan",      "on_event": { "PlanReady": "implement" } },
    "implement": {
      "prompt_task":   "implement",
      "max_visits":    5,
      "on_max_visits": "review",
      "artifacts": {
        "commit_sha":  { "type": "text/plain",       "description": "Latest commit" },
        "test_report": { "type": "application/json", "description": "Test runner output" }
      },
      "on_event": { "CodeReady": "test" }
    },
    "test":   { "prompt_task": "run_tests", "on_event": { "TestsFailed": "implement", "TestsPassed": "done" } },
    "review": { "prompt_task": "review",    "terminal": true },
    "done":   { "prompt_task": "summarize", "terminal": true }
  },
  "engine": {
    "budget": {
      "max_total_visits":  50,
      "max_tool_calls":   200,
      "max_wall_time_sec": 600
    }
  }
}
```

:::note[Loop guards vs. budgets]
`max_visits` bounds a single state — useful for "give the implementer up to 5 attempts before forcing review". `engine.budget` bounds the entire workflow — the runaway-loop safety net that catches cycles `max_visits` couldn't predict. Use both together: per-state caps for normal flow control, the budget as a backstop.
:::

---

## Agents *(v1.3+)*

PromptPack v1.3 adds agent definitions that map prompts to A2A (Agent-to-Agent) compatible agent cards. This enables multi-agent orchestration via the A2A protocol.

### AgentsConfig

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `entry` | string | **Yes** | Prompt key of the entry agent — the default agent that receives incoming requests. |
| `members` | object&lt;string, [AgentDef](#agentdef)&gt; | **Yes** | Map of prompt key to agent definition. Each key must match a prompt defined in the pack's `prompts` object. Must contain at least one entry. |

### AgentDef

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `description` | string | No | Agent description published in the A2A Agent Card. Overrides the prompt's description if set. |
| `tags` | string[] | No | Discovery tags for the agent, used by A2A registries and routers. |
| `input_modes` | string[] | No | MIME types the agent accepts as input. Defaults to `["text/plain"]`. |
| `output_modes` | string[] | No | MIME types the agent can produce as output. Defaults to `["text/plain"]`. |
| `state` | string | No | Reference to a state key in the pack's `workflow.states`. When set, invoking this agent runs the pack workflow starting at that state (following its transitions and loops) instead of executing the member-key prompt once. Requires a top-level `workflow`. If omitted, the agent is a single-prompt agent. See [RFC-0011](/docs/rfcs/workflow-states-as-agents). |
| `extensions` | object | No | Opaque policy annotations about this object. Never interpreted by this specification, and never passed to a scorer, guardrail or model as configuration. Keys SHOULD be namespaced. See [Policy annotation](#policy-annotation-v180). *(v1.8+)* |

```json
"agents": {
  "entry": "triage",
  "members": {
    "triage": {
      "description": "Routes customer requests to the right specialist",
      "tags": ["router", "customer-service"],
      "input_modes": ["text/plain"],
      "output_modes": ["text/plain"]
    },
    "billing": {
      "description": "Handles billing inquiries and payment issues",
      "tags": ["billing", "payments"],
      "input_modes": ["text/plain"],
      "output_modes": ["text/plain", "application/json"]
    },
    "technical": {
      "description": "Provides technical troubleshooting assistance",
      "tags": ["support", "technical"]
    }
  }
}
```

:::note[Workflow + Agents]
`workflow` and `agents` are independent features — you can use either or both. When used together, the workflow drives state transitions while agent definitions provide A2A discoverability metadata for each prompt.
:::

---

## Skills *(v1.3.1+)*

PromptPack v1.3.1 adds skills for progressive-disclosure knowledge loading. Skills are modular knowledge sources that agents load on demand, keeping system templates lean while providing access to deep domain expertise.

### SkillSource

Each entry in the top-level `skills` array is one of three forms:

| Form | Type | Description |
|------|------|-------------|
| **String** | string | Path to a skill directory/file or a package reference (e.g., `"./skills/billing"`, `"@acme/support-skills"`). |
| **SkillPathSource** | [SkillPathSource](#skillpathsource) | Path object with optional `preload` flag. |
| **InlineSkill** | [InlineSkill](#inlineskill) | Skill defined directly in the pack. |

### SkillPathSource

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `path` | string | **Yes** | Path to a skill directory, file, or package reference. |
| `preload` | boolean | No | If true, load eagerly at pack initialization. Default: `false`. |

### InlineSkill

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | **Yes** | Human-readable name for this skill. |
| `description` | string | **Yes** | Brief description of what this skill provides. |
| `instructions` | string | **Yes** | The skill's instructions or knowledge content. Loaded into the agent's context when activated. |

```json
"skills": [
  "./skills/billing",
  { "path": "./skills/compliance", "preload": true },
  {
    "name": "escalation-protocol",
    "description": "Steps for escalating unresolved customer issues",
    "instructions": "When a customer issue cannot be resolved within 3 exchanges:\n1. Acknowledge the complexity\n2. Collect case details\n3. Create an escalation ticket\n4. Provide the ticket reference to the customer"
  }
]
```

:::note[Skills + Workflow]
When a `WorkflowState` declares a `skills` field, it scopes which skills are available in that state. Use `"none"` to disable skills for a state. Without a `skills` field, all pack-level skills are available.
:::

---

## Compositions *(v1.5+)*

PromptPack v1.5 adds **workflow composition**: a workflow state can set `orchestration: composition` to drive its work with a declarative step graph instead of a single prompt. Compositions live in a new top-level `compositions` map, keyed by name, and are reached *only* through a workflow state. A purely procedural ("Function-mode") pack is a one-state terminal workflow whose state is in composition mode.

### WorkflowState amendments

Two backward-compatible changes to [WorkflowState](#workflowstate):

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `orchestration` | string | No | Now accepts a fourth value, `composition`, alongside `internal` / `external` / `hybrid`. `composition` delegates the state's *entire* orchestration (work + transitions) to the referenced composition. Exclusive — don't combine with the other modes on the same state. |
| `composition` | string | Conditional | Reference to a key in the pack's `compositions` map. **Required** when `orchestration` is `composition`; must be absent otherwise. |
| `prompt_task` | string | Conditional | Now **optional**. Still required for `internal` / `external` / `hybrid` (and the default `internal`); not used in `composition` mode. |

### Composition

A named step graph over the pack's prompts, tools, and evals.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `version` | integer | **Yes** | Composition format version. Currently `1`. |
| `steps` | [Step](#step)[] | **Yes** | Ordered array of step definitions (minimum 1). Order is logical — sequential by default; `branch` and `parallel` alter flow. The graph must be **acyclic**. |
| `description` | string | No | Human-readable description of what the composition does. |
| `input_schema` | string | No | Reference to a JSON Schema declaring the structured input shape. |
| `output_schema` | string | No | Reference to a JSON Schema declaring the structured output shape. |
| `output` | string | No | Step ID whose output is the composition's output. Defaults to the last step's output. |
| `engine` | object | No | Opaque runtime-specific configuration (budgets, telemetry, scheduling hints). No schema enforcement. |
| `extensions` | object | No | Opaque policy annotations about this object. Never interpreted by this specification, and never passed to a scorer, guardrail or model as configuration. Keys SHOULD be namespaced. See [Policy annotation](#policy-annotation-v180). *(v1.8+)* |

### Step

A single node in the step graph. The `kind` discriminator selects the step shape.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | **Yes** | Stable identifier, unique within the composition (including across nested `parallel.branches`). Pattern: `^[a-zA-Z_][a-zA-Z0-9_]*$`. Used for output references, eval attachment, and trace records. |
| `kind` | string | **Yes** | Step kind. v1 values: `"prompt"`, `"agent"`, `"tool"`, `"branch"`, `"parallel"`. Any other kind must be vendor-namespaced as `vendor.kind` (e.g. `"omnia.judge"`); an unnamespaced kind outside the v1 set is invalid. Fields beyond the common ones are defined by the runtime that supports the kind. *(namespacing enforced from v1.8)* |
| `description` | string | No | Human-readable description. |
| `depends_on` | string[] | No | Explicit predecessor step IDs. If omitted, the step sequentially follows the prior step in `steps[]`. Required to declare a join point after a `branch` or `parallel`. |
| `modifiers` | [StepModifiers](#stepmodifiers) | No | Declarative modifiers (retry, eval attachment). Semantics are runtime-defined. |

Each step also carries kind-specific fields, below.

#### PromptStep (`kind: "prompt"`)

A one-shot model invocation against a prompt task. No tool calls.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `kind` | string | **Yes** | Constant `"prompt"`. |
| `prompt_task` | string | **Yes** | Reference to a key in the pack's `prompts` object. |
| `input` | [StepInput](#stepinput) | No | Input binding resolved against the composition input and prior step outputs. |
| `provider` | string | No | Key of the [provider requirement](#provider-references-v190) that runs this step, overriding the `prompt_task`'s `provider`. *(v1.9.0+)* |
| `output_schema` | string | No | Reference to a JSON Schema for the expected output shape. |

#### AgentStep (`kind: "agent"`)

A **bounded LLM-tool loop** (distinct in name only from RFC 0007 `agents`).

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `kind` | string | **Yes** | Constant `"agent"`. |
| `prompt_task` | string | **Yes** | Reference to a key in the pack's `prompts` object. |
| `termination` | [TerminationPredicate](#terminationpredicate) | **Yes** | The condition under which the bounded loop exits. Without it, the agent step is invalid. |
| `input` | [StepInput](#stepinput) | No | Input binding. |
| `provider` | string | No | Key of the [provider requirement](#provider-references-v190) that runs this step, overriding the `prompt_task`'s `provider`. *(v1.9.0+)* |
| `tools` | string[] | No | Subset of the pack's tools available to this step — a per-step scoped tool registry. Each must resolve to a key in `tools`. |
| `output_schema` | string | No | Reference to a JSON Schema for the expected output shape. |

#### ToolStep (`kind: "tool"`)

A deterministic tool invocation called directly by the runtime (not via an LLM tool-call decision).

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `kind` | string | **Yes** | Constant `"tool"`. |
| `tool` | string | **Yes** | Reference to a key in the pack's `tools` object. |
| `args` | object | No | Argument bindings, resolved against the composition input and prior step outputs. |

#### BranchStep (`kind: "branch"`)

A conditional that picks a successor based on a constrained predicate.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `kind` | string | **Yes** | Constant `"branch"`. |
| `predicate` | [Predicate](#predicate) | **Yes** | Constrained predicate (no free-form expressions). |
| `then` | string | **Yes** | Step ID to execute when the predicate is true. |
| `else` | string | No | Step ID to execute when the predicate is false. |

#### ParallelStep (`kind: "parallel"`)

A static fan-out whose branches execute concurrently and are merged by a reducer.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `kind` | string | **Yes** | Constant `"parallel"`. |
| `branches` | [Step](#step)[] | **Yes** | At least **2** branch steps. |
| `reduce` | [Reducer](#reducer) | **Yes** | How branch outputs are merged. |

### Predicate

Predicates use a constrained, declarative shape — **not** an expression language. A predicate is exactly one of: a compare, an exists check, or an `all_of` / `any_of` / `not` combinator. Authors who need complex conditions emit a boolean from a `prompt` step and branch on its output.

| Form | Shape | Description |
|------|-------|-------------|
| **Compare** | `{ path, op, value }` | Compare a `path` against a literal `value`. `op` ∈ `equals`, `not_equals`, `in`, `not_in`, `less_than`, `less_than_or_equals`, `greater_than`, `greater_than_or_equals`. (`value` is an array for `in`/`not_in`.) |
| **Exists** | `{ path, exists }` | `exists: true`/`false` tests presence of the value at `path`. |
| **All of** | `{ all_of: [Predicate, …] }` | Logical AND of nested predicates. |
| **Any of** | `{ any_of: [Predicate, …] }` | Logical OR of nested predicates. |
| **Not** | `{ not: Predicate }` | Logical negation of a nested predicate. |

`path` references a value via the same `${...}` dot-notation used for step inputs, e.g. `${classify.output.intent}`.

```yaml
predicate:
  any_of:
    - path: "${assess.output.confidence}"
      op: less_than
      value: 0.8
    - path: "${assess.output.complexity}"
      op: greater_than
      value: 7
```

### Reducer

Names how a `parallel` block's branch outputs are merged into a single value.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `strategy` | string | **Yes** | v1 conventional values: `"append"` (extend lists), `"replace"` (last write wins), `"barrier"` (collect all outputs into a named map). Free-form string; runtimes may add vendor-namespaced reducers. |
| `into` | string | **Yes** | Field name under which the merged result is placed on the parallel step's output, read downstream as `${<parallelStepId>.output.<into>}`. |

### StepModifiers

Declarative annotations on a step. Semantics are runtime-defined.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `retry` | object | No | `{ "max_attempts": <integer ≥ 1> }` — retry budget for the step. |
| `eval` | string[] | No | References to keys in the pack's `evals` object (RFC 0006). Runtimes may execute these inline or post-Send. |

### TerminationPredicate

The required exit condition for an `agent` step. At least one field must be set.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `max_steps` | integer | Conditional | Maximum loop iterations (≥ 1). Either this or `tool_called` (or both) must be present. |
| `tool_called` | string | Conditional | Tool name; the agent terminates when the LLM successfully invokes this tool. |

### StepInput

Input binding for a step — either a reference string or an object combining literals and references.

| Form | Description |
|------|-------------|
| **String** | A reference of the form `${path.to.value}`. |
| **Object** | A free-form object whose values may be literals or `${...}` references. |

References resolve against:

- `${input.X}` — the composition's structured input.
- `${stepId.output.X}` — a prior step's structured output.

This is a strict subset of the RFC 0003 template-variable system — no expressions, arithmetic, or function calls.

:::note[Composition validation rules]
A `composition`-mode state must set `composition` and may omit `prompt_task`; every other state must set `prompt_task` and must not set `composition`. Step IDs must be unique within the composition; every `prompt_task`, `tool`, `eval`, `then`/`else`/`depends_on`, and `${...}` reference must resolve. `agent` steps require `termination`; `parallel` steps require ≥2 branches and a `reduce`; the graph must be acyclic.
:::

---

## Provider Requirements *(v1.5.1+)*

PromptPack v1.5.1 adds an optional top-level `requires` block so a pack can declare, runtime-agnostically, the model providers it needs to run. A requirement states *what the pack needs* (a logical provider, by key and role), never *which concrete provider satisfies it* — resolution is the host runtime's job. The block is fully backward compatible: optional, and validated strictly only when present.

### Requires

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `providers` | [ProviderRequirement](#providerrequirement)[] | No | Logical model-provider requirements. Each item is a string shorthand or a `ProviderRequirement` object. |

The `requires` object is closed (`additionalProperties: false`); `providers` is currently its only field, leaving room for future requirement categories (e.g. `requires.tools`).

### ProviderRequirement

Either a **string** (shorthand for `{ key: <string>, role: "llm", required: true }`) or an **object**:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | string | **Yes** | Logical name the runtime resolves this provider by (e.g. `default`, `embeddings`, `judge`). Pattern: `^[a-zA-Z0-9_-]+$`. `default` is reserved for the primary LLM. Keys must be unique within `providers` — `key` is the sole discriminator. |
| `role` | string | **Yes** | The kind of model required. **Open set** — validators must not reject unknown roles. Suggested values: `llm`, `embedding`, `tts`, `stt`, `image`, `inference`. |
| `required` | boolean | No | Whether the pack cannot run without this provider. Default `true`. An optional requirement degrades a feature rather than blocking startup. |
| `description` | string | No | Human-readable explanation of the provider's purpose and the capabilities it should have. The primary signal for an operator wiring things up. |
| `capabilities` | [ProviderCapabilities](#providercapabilities) | No | Structured, advisory capabilities for automatic matching. |

The object form is closed (`additionalProperties: false`).

### ProviderCapabilities

Structured, **advisory** hints the satisfying provider should have. The `description` remains the primary human guidance; `capabilities` lets runtimes match and warn automatically. The object is **open** (`additionalProperties: true`) — the well-known fields below are validated when present, but provider- or role-specific keys are allowed with any shape. Custom keys SHOULD be namespaced (e.g. an `x-` prefix) to avoid clashing with fields the spec may define later. All fields are optional.

| Field | Type | Description |
|-------|------|-------------|
| `modalities` | string[] | Media types the provider must handle. Reuses the RFC 0004 media vocabulary (`MediaConfig.supported_types`). Common: `text`, `image`, `audio`, `video`, `document`. |
| `min_context_tokens` | integer | Minimum context window, in tokens, the provider must support (≥1). |
| `tool_use` | boolean | Whether the provider must support tool/function calling. |
| `structured_output` | boolean | Whether the provider must support structured/JSON output. |
| `embedding_dimensions` | integer | Required embedding vector dimensionality, for `role: embedding` (≥1). |

:::note[Requirements vs. tested models]
`requires.providers` is the pack's **contract** (what it needs to run); `tested_models` is **provenance** (what a prompt was tested against). They're complementary and independent — a runtime MAY cross-check the provider it resolves against `tested_models` to warn on test/deploy divergence.
:::

### Provider references *(v1.9.0+)*

A requirement says *what* the pack needs. A **provider reference** says *where* each need is used: a `provider` property whose value is a requirement `key` ([RFC 0017](/docs/rfcs/provider-references)).

| Location | Meaning |
|---|---|
| `prompts.<key>.provider` | The requirement that runs this prompt. Absent means `default`. |
| `PromptStep.provider`, `AgentStep.provider` | Overrides the `prompt_task`'s provider for that step only. |

Everything else inherits from the prompt it runs. A workflow state, an in-process agent member, and a state exposed as an agent all use their prompt's `provider`. `tool`, `branch` and `parallel` steps make no model call and take no `provider`. For a composition step the precedence is: step `provider`, then the prompt's `provider`, then `default`.

```json
{
  "requires": {
    "providers": [
      "default",
      { "key": "triage", "role": "inference", "description": "Fast typed classifier." },
      { "key": "drafter", "role": "llm", "description": "Cheap model for templated replies." }
    ]
  },
  "prompts": {
    "classify_request": { "id": "classify_request", "name": "Classify request", "version": "1.0.0",
                          "provider": "triage", "system_template": "Customer message: {{input}}" },
    "refund_reply": { "id": "refund_reply", "name": "Refund reply", "version": "1.0.0",
                      "provider": "drafter", "system_template": "Write a refund confirmation for: {{input}}" }
  }
}
```

The host still binds each key to a concrete provider, and may bind `triage` to an LLM with structured output — the pack runs unchanged.

**Serving a call site.** A provider serves a call site when it can produce what the call site must produce:

| Call site | Must produce | Typically served by |
|---|---|---|
| Prompt in a conversational workflow state | A conversational reply | `role: llm` |
| Composition `agent` step | A bounded tool-calling loop | `role: llm` with `capabilities.tool_use` |
| Composition `prompt` step without `output_schema` | Free text | `role: llm` |
| Composition `prompt` step with `output_schema` | A JSON value conforming to the schema | `role: llm` with structured output, or any provider able to answer the schema |

A classifier or typed-decision provider can answer an `output_schema` whose every leaf is an enumerated choice, a bounded number or a boolean. How a runtime maps the schema onto such a provider is runtime-defined; the value handed to later steps must conform to `output_schema` either way.

**Rules.**

1. `provider` must equal a `key` in `requires.providers`, except `default`, which is always valid. JSON Schema cannot express the cross-reference; validators enforce it, as they do for `prompt_task`.
2. An absent `provider` is equivalent to `default` on a prompt and to the prompt's `provider` on a step.
3. A runtime must reject the pack at load time when the provider bound to a referenced key cannot serve a call site that references it — including when the key is unbound. It should say whether the key is undeclared (the pack author's error), unbound, or bound to a provider that cannot serve the call site (the host's errors).
4. Referencing a `required: false` requirement is valid, but if it is unbound at load time the pack is rejected: a call site that names it has nothing to fall back to.
5. Validators should warn when a conversational state's prompt, or a `prompt` step without `output_schema`, names a requirement whose `role` is not `llm`.

A runtime that ignores `provider` runs every call on the primary model. The output still conforms; latency and cost do not match what the author intended.

## Governance *(v1.6.0+)*

PromptPack v1.6.0 adds two optional blocks so a pack records **consequence** alongside the capability it already describes: `metadata.governance` for the agent as a whole, and `action_scope` on each tool. Both are declarations about the agent as designed — neither configures nor enforces anything. Fully backward compatible: every field is optional.

### Governance

Referenced from `metadata.governance` and from `AgentDef.governance`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `intended_purpose` | string | No | What the agent is built to do, stated by its author. |
| `foreseeable_misuse` | string[] | No | Uses the author considers out of bounds and reasonably foreseeable. |
| `autonomy_level` | string | No | `suggests`, `acts_with_approval`, `acts_with_oversight` or `acts_autonomously`. |
| `accountable_owner` | string | No | The role, team or function answerable for the agent. Prefer a durable identifier over a named individual. |
| `operator_role` | string | No | The declaring organisation's role, as a vocabulary term or free string (e.g. `eu-aiact:AIDeployer`). |
| `risk_classification` | string | No | The risk classification assigned, as a vocabulary term or free string. A namespaced term carries both framework and value. |
| `intended_deployment_contexts` | string[] | No | Sectors or settings the agent is built for. Distinct from `metadata.domain`, which is a discovery tag. |
| `capabilities` | string[] | No | Capabilities the agent exercises. Some carry obligations regardless of sector, so this is not covered by deployment contexts. |
| `approved_environments` | string[] | No | Environments the pack has been cleared to run in. Open strings — environment names are organisation-specific. |
| `requires_ai_disclosure` | boolean | No | Whether the agent must disclose that it is an AI. The runtime decides which of its interfaces this applies to. |
| `vocabularies` | object | No | Prefix → IRI map for CURIE values. `dpv`, `eu-aiact` and `ai` are well-known defaults and need not be declared. |
| `extensions` | object | No | Opaque annotations for external tooling. Never interpreted by this specification. Keys SHOULD be namespaced. |
| `obligations` | [Obligation](#obligation)[] | No | What obligations follow from the agent's declared capabilities, data or classification, and which controls discharge them. *(v1.8+)* |
| `reviews` | [Review](#review)[] | No | Obligations that recur: what is reviewed, how often, and which team owns it. *(v1.8+)* |
| `independent_of` | [IndependentOf](#independentof) | No | Requires that whatever produces this agent's input does not share the listed properties with it. *(v1.8+)* |

The object is closed (`additionalProperties: false`) — unrecognised information belongs in `extensions`.

**`autonomy_level` values**

| Value | Meaning |
|---|---|
| `suggests` | Produces output. A human performs any action. |
| `acts_with_approval` | Acts, but each consequential action is approved by a human first. |
| `acts_with_oversight` | Acts on its own. A human monitors and can intervene or reverse. |
| `acts_autonomously` | Acts without a human in the loop. |

### ActionScope

Referenced from `Tool.action_scope`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `effect` | string | No | `read` (changes nothing), `write` (changes operator-controlled state), `external` (effect outside the operator's systems; implies write). |
| `reversibility` | string | No | `reversible`, `compensable` (cannot be restored, but a defined compensating action limits harm), or `irreversible`. Declare against the world, not the API. |
| `data_classes` | string[] | No | Classes of data the tool touches, as vocabulary terms or free strings. |
| `extensions` | object | No | Opaque annotations about consequence — a blast radius, a severity score. Never interpreted by this specification. |

`Tool` also gains its own `extensions` bag, for annotations that are not about consequence — including hints a runtime needs for its own policy (`acme.example/approval-threshold: 10000`).

### Why the values are closed

`autonomy_level`, `effect` and `reversibility` are **closed enums** because their whole value is that a policy expression can depend on them. "Approve anything irreversible" is durable only if `irreversible` means one thing across packs from different authors.

`risk_classification`, `intended_deployment_contexts`, `capabilities`, `data_classes` and `operator_role` are **open** because their content is legal and sectoral taxonomy that PromptPack has no business maintaining. `approved_environments` and `accountable_owner` are open because only the declaring organisation can name those things.

:::note[Absence is not a safe default]
An omitted field means **undeclared**. An omitted `action_scope` does not mean `read`, and does not mean `reversible`. An omitted `approved_environments` means neither "cleared everywhere" nor "cleared nowhere". A runtime that gates on these values SHOULD treat undeclared as the most conservative class it supports, and MUST document that choice.
:::

:::note[Declarations are asserted, not verified]
Nothing checks that a tool marked `reversible` can be reversed, or that `risk_classification` reflects a real assessment. These are assertions, exactly as `description` is. Recording *who* declared a value, when, and under what review is deliberately deferred to a future RFC.
:::

### Obligations, reviews and independence *(v1.8.0+)*

v1.8.0 adds the facts a regulated reader asks for after RFC 0013's: which obligation a declared capability triggers and what discharges it, which obligations recur, and that a reviewing agent must be independent of what it reviews (RFC 0016). All three are declarations; none enforces anything.

#### Obligation

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | **Yes** | Unique within the governance object that declares it. Referenced by `reviews[].satisfies`. |
| `obligation` | string | **Yes** | The obligation, as a vocabulary term or free string (e.g. `eu-aiact:Article50`). |
| `applies_to` | object | No | What triggers it: any of `capability`, `data_class`, `risk_classification`. A record, not a filter — whether an obligation applies is a legal determination. |
| `controls` | [ObligationControl](#obligationcontrol)[] | **Yes** | At least one control that discharges the obligation. |
| `note` | string | No | Free-text context, such as the article the obligation comes from. |
| `extensions` | object | No | Opaque annotations — a control-framework id, an internal risk rating. Never evidence that the obligation is current. |

#### ObligationControl

Exactly one key per entry. They are four different kinds of claim, and most real obligations want more than one.

| Key | Resolves against | What it says |
|-----|------------------|--------------|
| `field` | a property of `governance` | *This is declared.* The named field must be declared in the effective governance object. |
| `validator` | a `Validator.id` on one of the pack's prompts | *This is enforced in the response path.* Validators always enforce (RFC 0015). |
| `eval` | an eval `id` in the pack's or a prompt's `evals` | *This is watched.* Records that a measurement exists, not its outcome; what acts on the score is runtime policy. |
| `external` | nothing | *This is handled somewhere the pack cannot point at.* Prose. |

An eval named here needs no `threshold`. A pass/fail measurement declares `metric.type: boolean`; a score is the measurement, and the judgement about it belongs to whatever consumes the metric.

#### Review

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `id` | string | **Yes** | Unique within the governance object. Keep it stable across pack versions while the review means the same thing — runtimes key completion records by it. |
| `type` | string | **Yes** | The kind of review, as a vocabulary term or free string (e.g. `pp:BiasTesting`, `risk:RiskAssessment`). |
| `cadence` | string | **Yes** | ISO 8601 duration between reviews (`P3M`, `P1Y`, `PT12H`). Not the empty duration `P`. |
| `owner` | string | **Yes** | The team or role that owns the review. Never a named individual. |
| `satisfies` | string[] | No | Ids of obligations in the effective governance object that this review answers to. |
| `eval` | string | No | An eval `id` that this review runs or reads. |
| `extensions` | object | No | Opaque annotations — method reference, evidence location, escalation route. MUST NOT be used to record completions. |

`reviews` is a sibling of `obligations` because one review routinely satisfies several obligations. The pack declares cadence and owner; **whether a review actually happened is runtime state**, so the pack alone cannot answer "is this obligation current?".

#### IndependentOf

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `axes` | string[] | **Yes** | At least one of `model`, `provider`, `tools`, `prompts`, `accountable_owner`, no duplicates. |
| `enforcement` | string | No | `strict` or `advisory` (default). Under `strict`, a runtime that enforces this MUST NOT deploy the pack when the requirement is unsatisfied — including when it cannot determine the producer. |

The requirement names no counterparty: the producer is usually in another pack, and the runtime resolves it from the composition it is running.

| Axis | Resolves against | Buys |
|---|---|---|
| `model` | the effective model after `model_overrides` | technical independence |
| `provider` | the effective provider | technical, plus insulation from one provider's outage |
| `tools` | the tool sets — independent when disjoint | technical; a shared tool is a shared blast radius |
| `prompts` | prompt keys — independent when neither uses the other's | technical; shared instructions correlate judgement |
| `accountable_owner` | the `accountable_owner` of each | **organisational** — a different team answers for the review |

:::caution[The technical axes are a quality control, not a security control]
They guard against correlated failure, not compromise. A pack that wants segregation of duties must name `accountable_owner`; the technical axes alone buy diversity of judgement, not separation of responsibility.
:::

#### Reference rules

These are what stop an obligation being satisfied by naming something that does not exist. A validator implementing Level 1 MUST report each as an error:

1. `controls[].eval` and `reviews[].eval` resolve to an eval `id` in the pack's `evals` or a prompt's `evals`.
2. `controls[].validator` resolves to a `Validator.id`; `Validator.id` values are unique across the pack.
3. `controls[].field` names a governance property that is declared in the effective governance object.
4. `reviews[].satisfies` entries resolve to `obligations[].id` in the effective governance object.
5. `obligations[].id` and `reviews[].id` are each unique within the object that declares them.

Terms in `obligation`, `applies_to.*` and `review.type` follow the CURIE rule above: an undeclared prefix warns and never errors; a value with no colon is a free string. `AgentDef.governance` replaces `obligations`, `reviews` and `independent_of` whole, like every other array.

### Well-known prefixes *(v1.8.0+)*

None of these need declaring in `vocabularies`. All external namespaces are unversioned `w3id.org` IRIs, which follow the current DPV release. The table saves boilerplate and is not a gate — any prefix works if declared, and an undeclared one only warns.

| Prefix | Namespace | Supplies |
|---|---|---|
| `dpv` | `https://w3id.org/dpv#` | roles, `PersonalData`, `SpecialCategoryPersonalData`, `AutomatedDecisionMaking` *(v1.6)* |
| `eu-aiact` | `https://w3id.org/dpv/legal/eu/aiact#` | AI Act actor roles and risk levels *(v1.6)* |
| `ai` | `https://w3id.org/dpv/ai#` | AI capabilities and techniques *(v1.6)* |
| `pd` | `https://w3id.org/dpv/pd#` | personal-data categories, for `data_classes` |
| `risk` | `https://w3id.org/dpv/risk#` | risk taxonomy, including risk-management `reviews[].type` |
| `tech` | `https://w3id.org/dpv/tech#` | technology concepts |
| `legal-eu-gdpr` | `https://w3id.org/dpv/legal/eu/gdpr#` | GDPR legal concepts |
| `legal-us` | `https://w3id.org/dpv/legal/us#` | US state privacy laws and authorities |
| `sector-health`, `sector-finance`, `sector-education`, `sector-law`, `sector-publicservices`, `sector-infra` | `https://w3id.org/dpv/sector/<name>#` | `intended_deployment_contexts` |
| `hipaa` | [`https://promptpack.org/vocab/hipaa#`](/vocab/hipaa/) | minted: `CoveredEntity`, `BusinessAssociate`, `PHI`, `ePHI`, `LimitedDataSet`, `DeIdentified` |
| `pp` | [`https://promptpack.org/vocab/pp#`](/vocab/pp/) | minted: `Confidential`, `Restricted`, `Public`, `Credentials`, `FinancialAccount`, `SyntheticMedia`, and the review practices `BiasTesting`, `AccuracyReview`, `RedTeaming`, `DataQualityReview`, `HumanOversightReview` |

A term is minted under `promptpack.org` only where no external vocabulary supplies it **and** its meaning is fixed — by the instrument that defines it (`hipaa:`, each term linking to its CFR section), or by PromptPack's own definition, which is never revised in place (`pp:`). Both namespaces are published as [Turtle, JSON-LD and a readable page](/vocab/). A prefix identifies a concept; it does not interpret the instrument, and declaring it makes nothing compliant with anything.

## Policy annotation *(v1.8.0+)*

Wherever a runtime policy makes a decision, it needs somewhere to keep what it knows. v1.8.0 settles which definitions get an `extensions` slot by rule rather than case by case (RFC 0016): **a definition gets `extensions` if a policy could make a decision at it, and it occurs as one of many.**

| Definition | The decision a policy makes there | Since |
|---|---|---|
| `Tool` | whether this call is allowed | v1.6 |
| `ActionScope` | what consequence the call carries | v1.6 |
| `Governance` | whether this agent may run at all | v1.6 |
| `Prompt` | the point the model is invoked | v1.8 |
| `Validator` | whether to gate the response | v1.8 |
| `Eval` | what to measure, and what to do with the score | v1.8 |
| `AgentDef` | which agent runs, and where | v1.8 |
| `WorkflowState` | whether a transition is allowed | v1.8 |
| `Composition` | how the orchestration is admitted | v1.8 |

The contract is the same everywhere: any JSON object, keys SHOULD be namespaced, and a conforming implementation MUST NOT validate or interpret the contents, or pass them to a scorer or guardrail as configuration. That last point is why annotation does not belong in `params` — `params` is configuration the runtime executes; `extensions` is data it only carries.

Plumbing definitions (`Variable`, `Parameters`, `ModelOverride`, `TestedModel`, the media configs) stay closed, as do singletons whose parent already has a slot (`ToolPolicy`). When a namespaced key appears across independent authors with a consistent meaning, it is a candidate for promotion to a real field.

---

## Data Types

### Supported Variable Types

- **string** -- Text data
- **number** -- Numeric values (integers and floats)
- **boolean** -- True/false values
- **array** -- Ordered lists of values
- **object** -- Key-value maps

### Template Variables

Variables in templates use the syntax defined in `template_engine.syntax`. With the default `{{variable}}` syntax:

```
{{variable_name}}
```

Fragments are referenced similarly:

```
{{fragments.fragment_name}}
```
