# Pack Structure & Design

Understanding how PromptPacks are structured helps you design better AI agent behavior — from simple multi-prompt routers to autonomous agent loops. The JSON-based format isn't just about data storage; it's architected to support real-world agent development patterns and deployment needs.

## The Multi-Prompt Architecture

A defining characteristic of PromptPacks is that **each pack can contain multiple specialized prompts** — one per workflow stage, one per routing destination, or one per agent — rather than trying to fit everything into a single generic prompt.

### Why Multiple Prompts?

**Better Performance**: Specialized prompts outperform generic ones. A prompt optimized for technical support will handle troubleshooting better than a generic "customer service" prompt trying to do everything.

**Easier Maintenance**: When you need to improve sales conversations, you modify just the sales prompt without affecting support or technical prompts.

**Independent Evolution**: Each prompt can have its own version, testing results, and optimization path while sharing common infrastructure.

**Clear Separation of Concerns**: Different prompts can use different tools, have different safety rules, and target different LLM parameters.

### Pack Organization

```json
{
  "id": "customer-support",
  "name": "Customer Support Pack",
  "version": "1.0.0",
  
  "prompts": {
    "support": { /* optimized for general support */ },
    "sales": { /* tuned for sales conversations */ },
    "technical": { /* focused on troubleshooting */ },
    "billing": { /* handles payment issues */ }
  }
}
```

Each prompt operates independently but shares the pack's common resources.

## Shared Resources Architecture

PromptPacks eliminate duplication through shared resources that all prompts can use:

### Template Engine Configuration

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

**Why Shared**: Ensures all prompts use the same templating system, making the pack portable across different runtime environments.

### Reusable Fragments

```json
{
  "fragments": {
    "company_intro": "Welcome to {{company_name}}, where customer satisfaction is our priority.",
    "escalation_notice": "Let me connect you with a specialist who can better assist you.",
    "data_privacy": "We protect your information according to our privacy policy."
  }
}
```

**Benefits**:

- **Consistency**: Same messaging across all prompts
- **Maintainability**: Update company intro once, affects all prompts
- **Localization**: Easy to swap fragments for different languages or regions

### Tool Definitions

```json
{
  "tools": {
    "lookup_customer": { /* database query tool */ },
    "create_ticket": { /* ticketing system integration */ },
    "send_email": { /* email automation tool */ }
  }
}
```

**Advantages**:

- **Reusability**: Define once, reference from any prompt
- **Security**: Centralized tool policies and permissions
- **Testing**: Tool behavior is consistent across all prompts

## Prompt-Level Specialization

While sharing common resources, each prompt has its own specialized configuration:

### Independent Parameters

```json
{
  "prompts": {
    "support": {
      "parameters": {
        "temperature": 0.7,
        "max_tokens": 1500
      }
    },
    "sales": {
      "parameters": {
        "temperature": 0.9,
        "max_tokens": 800
      }
    }
  }
}
```

### Specialized Tool Access

```json
{
  "prompts": {
    "support": {
      "tools": ["lookup_customer", "create_ticket", "send_email"]
    },
    "sales": {
      "tools": ["lookup_customer", "get_pricing", "schedule_demo"]
    },
    "billing": {
      "tools": ["lookup_customer", "process_payment", "send_receipt"]
    }
  }
}
```

### Custom Validation Rules

```json
{
  "prompts": {
    "support": {
      "validators": [
        {"type": "max_length", "params": {"max_tokens": 500}},
        {"type": "banned_words", "params": {"words": ["impossible", "can't help"]}}
      ]
    },
    "sales": {
      "validators": [
        {"type": "sentiment", "params": {"min_positive": 0.6}},
        {"type": "max_length", "params": {"max_tokens": 300}}
      ]
    }
  }
}
```

## Version Management Strategy

PromptPacks support both pack-level and prompt-level versioning:

```json
{
  "version": "2.1.0",
  "prompts": {
    "support": {
      "version": "1.5.2"
    },
    "sales": {
      "version": "2.0.1"
    }
  }
}
```

**Benefits**:

- **Granular Updates**: Update just the sales prompt without changing support
- **Rollback Capability**: Revert individual prompts to previous versions
- **A/B Testing**: Run different prompt versions simultaneously
- **Compatibility Tracking**: Know which prompt versions work together

## Testing and Quality Assurance

PromptPacks include built-in support for testing and quality tracking:

```json
{
  "prompts": {
    "support": {
      "tested_models": [
        {
          "provider": "openai",
          "model": "gpt-4",
          "success_rate": 0.94,
          "avg_cost": 0.0045,
          "avg_latency_ms": 1200
        }
      ]
    }
  }
}
```

This enables:

- **Model Selection**: Choose the best-performing model for each prompt
- **Cost Optimization**: Balance performance vs. cost across different prompts
- **Performance Monitoring**: Track how prompts perform over time
- **Deployment Confidence**: Know before you deploy whether a prompt works well

### Evals *(v1.2+)*

PromptPacks can also declare **evals** — automated quality checks that run asynchronously and produce metrics. Evals can be defined at pack level (applying to all prompts) or prompt level (scoped to a specific prompt):

```json
{
  "evals": [
    {
      "id": "json_format",
      "type": "json_valid",
      "trigger": "every_turn",
      "metric": { "name": "promptpack_json_valid", "type": "boolean" }
    }
  ]
}
```

Unlike validators (which block output), evals score and report — making them ideal for continuous quality monitoring with Prometheus-style metric export.

## Workflow Orchestration *(v1.3+)*

PromptPack v1.3 introduces a state-machine workflow that orchestrates transitions between prompts based on events. Instead of a caller manually choosing which prompt to invoke, the workflow defines an entry state and event-driven transitions:

```json
{
  "workflow": {
    "version": 1,
    "entry": "triage",
    "states": {
      "triage": {
        "prompt_task": "triage",
        "on_event": { "billing": "billing_support", "technical": "tech_support" }
      },
      "billing_support": {
        "prompt_task": "billing",
        "on_event": { "resolved": "closing" },
        "persistence": "persistent"
      },
      "tech_support": {
        "prompt_task": "technical",
        "on_event": { "resolved": "closing" },
        "persistence": "persistent"
      },
      "closing": {
        "prompt_task": "closing",
        "on_event": {}
      }
    }
  }
}
```

Each state references a prompt key and declares which events trigger transitions to other states. States can be `transient` (context reset on entry) or `persistent` (context preserved), and orchestration can be `internal`, `external`, or `hybrid`.

## Agent Definitions *(v1.3+)*

The `agents` section maps prompts to A2A (Agent-to-Agent) protocol compatible agent cards, enabling multi-agent discovery and orchestration:

```json
{
  "agents": {
    "entry": "triage",
    "members": {
      "triage": {
        "description": "Routes requests to specialists",
        "tags": ["router"],
        "input_modes": ["text/plain"],
        "output_modes": ["text/plain"]
      },
      "billing": {
        "description": "Handles billing inquiries",
        "tags": ["billing", "payments"]
      }
    }
  }
}
```

Each agent definition provides metadata for the A2A Agent Card — description, discovery tags, and supported MIME types. The `entry` field identifies which agent receives incoming requests by default.

## Skills *(v1.3.1+)*

The `skills` section declares external knowledge sources that agents can load progressively on demand. Instead of embedding all domain knowledge in system templates, skills let you keep templates lean and load expertise when it's relevant.

```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"
    }
  ]
}
```

Skills come in three forms:

- **String paths** — reference a directory or package (`"./skills/billing"`, `"@acme/support-skills"`)
- **Path objects** — provide a path with optional `preload: true` for eager loading
- **Inline skills** — define `name`, `description`, and `instructions` directly in the pack

When combined with workflows, each state can declare a `skills` field to scope which skills are available in that context, or use `"none"` to disable skills entirely for a state.

## Agent Loops *(v1.4+)*

PromptPack v1.4 extends `WorkflowState` with the fields needed to express iterative, self-correcting execution patterns over the existing state machine — without leaving the spec. The same `workflow` block now supports terminal states, bounded loops, structured artifacts that flow across visits, and an engine-level execution budget.

```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 generated commit" },
          "test_report": { "type": "application/json", "description": "Test runner summary" },
          "log":         { "type": "text/plain", "mode": "append", "description": "Iteration log" }
        },
        "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 }
    }
  }
}
```

Four building blocks turn an unbounded workflow into a production-safe agent loop:

- **`terminal: true`** — marks states that exit the workflow explicitly. A terminal state should not declare `on_event` transitions.
- **`max_visits` (per state)** — caps how many times a single state can be entered during one execution. When the limit is hit, the workflow transitions to `on_max_visits` if set, or terminates with a budget-exhausted status.
- **`artifacts`** — named slots for lightweight, structured metadata (commit SHAs, file paths, JSON summaries, diffs). Each entry declares a MIME `type`, an optional `description`, and an optional `mode`: `"replace"` (default — overwrite on each visit) or `"append"` (accumulate across visits, e.g. a log). Values are accessible to prompts as `{{artifacts.<name>}}`.
- **`engine.budget`** — a global safety net independent of per-state caps. Supports `max_total_visits`, `max_tool_calls`, and `max_wall_time_sec`. Reaching any limit terminates the workflow with a budget-exhausted status.

:::note[Time-travel debugging for free]
Because artifacts are captured at every state transition, runtimes that persist them produce a structured, replayable execution trace. You get audit, replay, and step-back debugging without writing any extra orchestration code.
:::

## Turn Control *(v1.7+)*

Every state above yields to the user after a transition. The agent calls `workflow__transition`, the state changes, and the conversation waits. That is right for interactive flows and wrong for **transient states** — decision points that exist only to route, and processing steps in a loop that shouldn't stop to ask permission between rounds.

PromptPack v1.7 adds `control` to `WorkflowState` (RFC 0014). It declares who holds the next turn after entering the state:

- **`control: "user"`** (default) — yield the conversation to the user. A state without the property does this.
- **`control: "agent"`** — run another agent round in this state instead of yielding, rebuilding the pipeline for the new state's `prompt_task`, tools, and validators.

:::note[Adopting the default can change behavior]
Before v1.7 the specification did not say who holds the turn after a transition, and implementations differed. PromptKit, for one, already continued into `internal` and `hybrid` destination states without yielding. A runtime like that which adopts `user` as the meaning of an absent `control` turns every such routing state into a stop, so packs relying on the old behavior need `control: "agent"` declared explicitly.
:::

```json
{
  "workflow": {
    "version": 2,
    "entry": "verifying",
    "states": {
      "verifying": {
        "prompt_task": "verifying",
        "on_event": { "AccountVerified": "triage" }
      },
      "triage": {
        "prompt_task": "triage",
        "control": "agent",
        "on_event": { "ServeBalance": "resolution", "EscalateToAgent": "handoff" }
      },
      "resolution": { "prompt_task": "resolution", "terminal": true },
      "handoff":    { "prompt_task": "handoff",    "terminal": true }
    }
  }
}
```

A caller who says "I need my balance, my ID is ACC-12345" is verified, routed through `triage`, and answered in `resolution` — one user message, one reply, three states. Without `control`, `triage` would ask the user to speak again before it could route on what they already said.

**`control` is not `orchestration`.** They answer different questions and compose freely:

| Property | Question it answers | Values |
|---|---|---|
| `orchestration` | Who *initiates* the transition? | `internal`, `external`, `hybrid`, `composition` |
| `control` | Who holds the turn *after* one? | `user`, `agent` |

`control` is consulted only on an agent-initiated transition, so it is inert on a state reached through `external` orchestration — there is no turn in hand to give back.

Loops of `control: agent` states need no new safety net: they terminate on reaching a `control: "user"` state or a terminal one, and are bounded by the same `max_visits` and `engine.budget` limits described above. A runtime that doesn't implement `control` yields as it always did, so packs using it stay portable — they just take more user turns.

## Workflow Composition *(v1.5+)*

PromptPack v1.5 adds a third way to drive a workflow state: a **composition**. Until now a `WorkflowState` was always backed by a single prompt (`prompt_task`) and orchestrated `internal`ly, `external`ly, or via `hybrid` control. v1.5 adds `composition` as a fourth `orchestration` value. When a state sets `orchestration: composition`, its work is driven by a **declarative step graph** instead of a single LLM call — ideal for *procedural* flows (document pipelines, data extraction, request → reasoning → commit) that don't fit the event-driven dialogue shape.

Compositions live in a new top-level `compositions` map, keyed by name, exactly like `prompts`, `tools`, and `evals`. A state references one by key:

```json
{
  "workflow": {
    "version": 1,
    "entry": "main",
    "states": {
      "main": {
        "orchestration": "composition",
        "composition": "analyze_document",
        "terminal": true
      }
    }
  },
  "compositions": {
    "analyze_document": {
      "version": 1,
      "description": "Classify a document and route to a type-specific analyzer.",
      "steps": [
        { "id": "classify", "kind": "prompt", "prompt_task": "doc_classifier", "input": "${input.text}" },
        {
          "id": "route", "kind": "branch",
          "predicate": { "path": "${classify.output.type}", "op": "equals", "value": "research_paper" },
          "then": "extract_paper", "else": "extract_general"
        },
        { "id": "extract_paper",   "kind": "prompt", "prompt_task": "research_paper_extractor", "input": "${input.text}" },
        { "id": "extract_general", "kind": "prompt", "prompt_task": "general_doc_extractor",   "input": "${input.text}" }
      ]
    }
  }
}
```

Two amendments make this work, both fully backward compatible:

- **`orchestration` gains a `composition` value** alongside `internal` / `external` / `hybrid`. Selecting it delegates the state's *entire* orchestration to the referenced composition — work and transitions both. It is exclusive: don't mix `composition` with the other modes on the same state.
- **`prompt_task` becomes optional.** It's still required for `internal` / `external` / `hybrid` (and the default `internal`); a `composition`-mode state omits it and sets `composition` instead.

A composition is a directed **acyclic** graph of typed steps. v1 defines five step kinds:

- **`prompt`** — a one-shot model call against a `prompt_task`, with an optional `output_schema`. No tool calls.
- **`agent`** — a *bounded LLM-tool loop* over a scoped `tools` list. **Requires** a `termination` predicate (`max_steps` and/or `tool_called`).
- **`tool`** — a deterministic tool invocation called directly by the runtime (not via an LLM tool-call decision).
- **`branch`** — picks `then`/`else` based on a constrained `predicate` (no expression language).
- **`parallel`** — a static fan-out of **≥2 branches** merged by a declared `reduce` strategy (`append` / `replace` / `barrier`).

A runtime may support further kinds, written `vendor.kind` (`omnia.judge`). The dot is required from v1.8: it keeps vendor kinds clear of any kind a future version of the specification defines, and a composition using one is portable only to runtimes that support it.

Steps wire together with reference bindings — `${input.X}` reads the composition's structured input; `${stepId.output.X}` reads a prior step's output. Optional per-step `modifiers` add `retry` (max attempts) and `eval` (attach pack-level eval keys). Compositions are reached *only* through a workflow state, so a purely procedural ("Function-mode") pack is simply a one-state terminal workflow whose state is in composition mode — the workflow state machine remains the universal orchestration primitive.

See the [Compositions schema reference](./schema-guide#compositions-v15), the [worked example](./examples#procedural-document-analyzer-with-composition-v15), and [How to Add a Composition](/docs/guides/add-composition) for the full vocabulary.

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

A pack is portable, but its model-provider needs have always been implicit — every runtime and deployment rediscovered "this needs an LLM, and an embedding model for retrieval, and maybe a judge model" by hand, and failed late when a binding was missing or pointed at the wrong kind of model. PromptPack v1.5.1 adds an optional top-level `requires` block so a pack can declare those needs once, runtime-agnostically:

```json
{
  "requires": {
    "providers": [
      "default",
      {
        "key": "embeddings",
        "role": "embedding",
        "description": "Embeds the knowledge base for retrieval.",
        "capabilities": { "embedding_dimensions": 1536 }
      },
      {
        "key": "judge",
        "role": "llm",
        "required": false,
        "description": "Optional LLM judge for the eval suite."
      }
    ]
  }
}
```

Each entry under `requires.providers` is either a **string shorthand** (a bare key, which expands to a required `llm` requirement) or a **`ProviderRequirement` object**:

- **`key`** — the logical name the runtime resolves the provider by (`default`, `embeddings`, `judge`, …). `default` is reserved for the primary LLM. Keys must be unique within the list — `key` is the sole discriminator, so a fast and a strong model are just two `llm` requirements with different keys.
- **`role`** — the *kind* of model (`llm`, `embedding`, `tts`, `stt`, `image`, `inference`, …). The set is **open**: validators must not reject unknown roles.
- **`required`** — defaults to `true`. An optional requirement (`false`) degrades a feature rather than blocking startup.
- **`description`** — human guidance on the provider's purpose and the capabilities it should have. This stays the primary signal for an operator wiring things up.
- **`capabilities`** — optional, advisory, structured hints for automatic matching: `modalities` (reusing the RFC 0004 media vocabulary), `min_context_tokens`, `tool_use`, `structured_output`, `embedding_dimensions`. The object is **open** — provider-specific keys are allowed and SHOULD be namespaced (e.g. an `x-` prefix) to avoid clashing with fields the spec may define later.

A requirement declares *what the pack needs*, never *which concrete provider satisfies it* — resolution is the host runtime's job. The block is fully backward compatible (optional; validated strictly only when present) and complements `tested_models`: `tested_models` records provenance (what a prompt was tested against), `requires.providers` records the contract (what the pack needs), so a runtime can warn when the resolved provider diverges from what the pack was tested on. See the [Provider Requirements schema reference](./schema-guide#provider-requirements-v151).

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

Declaring a requirement does not say which calls use it. From v1.9.0, a prompt — or a composition `prompt` or `agent` step — names one with `provider`:

```json
{
  "prompts": {
    "classify_request": { "...": "...", "provider": "triage" },
    "refund_reply": { "...": "...", "provider": "drafter" },
    "support_agent": { "...": "..." }
  }
}
```

An absent `provider` means `default`, which is what every call used before. Workflow states and agent members inherit the provider of the prompt they run; a composition step may override its prompt's provider for that step. The host still binds each key to a concrete provider, and must reject the pack at load time if a referenced key is unbound or bound to a provider that cannot serve the call site. A `prompt` step with an `output_schema` whose leaves are all choices, bounded numbers or booleans may be served by a non-LLM provider such as a classifier. See [Provider references](./schema-guide#provider-references-v190) and [RFC 0017](/docs/rfcs/provider-references).

## Governance Declarations *(v1.6.0+)*

A pack has always described **capability** in detail and **consequence** not at all. It carries the model, the instructions, the tools, the guardrails and the test evidence — but nothing states what the agent is *for*, what it must not be used for, who answers for it, or what happens when a tool actually runs. Those facts existed, in wikis and spreadsheets and ticket fields, none of them versioned with the pack they describe.

PromptPack v1.6.0 adds two optional blocks so they live with the thing they describe.

### `metadata.governance`

```yaml
metadata:
  domain: finance
  governance:
    intended_purpose: >
      Answers cardholder questions about settled transactions and raises
      disputes on the cardholder's explicit instruction.
    foreseeable_misuse:
      - Credit, pricing or eligibility decisioning
      - Adjudicating a dispute without a human reviewer
    autonomy_level: acts_with_approval
    accountable_owner: payments-risk
    operator_role: eu-aiact:AIDeployer
    approved_environments: [staging, production]
    requires_ai_disclosure: true
```

- **`intended_purpose`** / **`foreseeable_misuse`** — what the agent is built to do, and the uses its author considers out of bounds.
- **`autonomy_level`** — `suggests` (a human acts), `acts_with_approval` (each consequential action is approved first), `acts_with_oversight` (acts alone, a human can intervene or reverse), `acts_autonomously`. The split between approval and oversight is a gate *before* the fact versus a check *after* it.
- **`accountable_owner`** — the role or team answerable for the agent. Prefer a durable identifier over a named individual, who changes team without the agent changing.
- **`operator_role`**, **`risk_classification`**, **`intended_deployment_contexts`**, **`capabilities`** — open values, expressed as a CURIE (`eu-aiact:AIDeployer`) or an absolute IRI. The spec neither resolves nor validates terms; `vocabularies` maps prefixes so a consumer can if it wants to. DPV is recommended, never required, and a value that is not a CURIE remains a valid free string.
- **`approved_environments`** — where the pack has been cleared to run. Lets a runtime refuse a staging-only pack in production.
- **`requires_ai_disclosure`** — whether the agent must tell people it is an AI. The runtime decides which of its interfaces that applies to.

### `Tool.action_scope`

```yaml
tools:
  lookup_transaction:
    description: Look up a settled transaction by reference
    action_scope:
      effect: read
      data_classes: [dpv:FinancialData]
  issue_refund:
    description: Issue a refund against a settled transaction
    action_scope:
      effect: external
      reversibility: compensable
      data_classes: [dpv:FinancialData]
```

- **`effect`** — `read` (changes nothing), `write` (changes state the operator controls), `external` (causes an effect outside the operator's systems; implies write).
- **`reversibility`** — `reversible`, `compensable` (the prior state cannot be restored, but a defined compensating action limits the harm), or `irreversible`. Declare it against the world, not the API.
- **`data_classes`** — what the tool touches, as vocabulary terms or free strings.

This is what earns the block its keep. A rule as ordinary as "stop and ask a human before anything that cannot be undone" previously had to be written as a **list of tool names** — correct the day it was written and silently wrong the next time an irreversible tool was added. With `action_scope`, the policy is written against consequence and stays correct.

### Declared intent, not enforced configuration

Every field here states something about the agent as designed. None of them configures anything. `autonomy_level: acts_with_approval` says the pack was built and tested to act behind a human gate; it does not create the gate. `approved_environments` says where the agent is cleared; it does not deploy it. Enforcement lives in `tool_policy`, in runtime policy and in the deployment pipeline — any of which may be stricter than the pack asks while the pack still describes the agent correctly.

**Absence 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 document that choice.

`AgentDef.governance` carries the same shape for a single agent, overriding `metadata.governance` by **per-field replacement** — a field present on the agent replaces the pack value; a field absent inherits. Arrays and `extensions` replace whole, because deep-merging `foreseeable_misuse` across two levels would produce a list no one wrote.

`vocabularies` is the exception and merges. It is a prefix-to-IRI map that makes CURIEs resolvable rather than a declaration in its own right, so replacing it would put an agent's *inherited* values out of scope — a pack declaring `acme:` and a `risk_classification: acme:tier-3`, under an agent declaring only `other:`, would leave that inherited value unresolvable. Prefixes accumulate; an agent that redeclares one still wins on it.

See the [Governance schema reference](./schema-guide#governance-v160).

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

`capabilities` is a list of labels. v1.8.0 lets a pack say what each label obliges, what discharges it, which duties recur, and who must not be reviewing whom (RFC 0016):

```yaml
metadata:
  governance:
    capabilities: [dpv:AutomatedDecisionMaking]
    autonomy_level: acts_with_approval
    accountable_owner: lending-risk
    obligations:
      - id: art22-human-review
        obligation: legal-eu-gdpr:Article22
        applies_to: { capability: dpv:AutomatedDecisionMaking }
        controls:
          - field: autonomy_level             # declared
          - validator: decline-reasons-guard  # enforced in the response path
          - eval: decline-carries-reasons     # watched
          - external: Underwriter queue with four-hour SLA
    reviews:
      - id: quarterly-bias-test
        type: pp:BiasTesting
        cadence: P3M
        owner: fair-lending-team
        satisfies: [art22-human-review]
        eval: demographic-parity
```

- **`obligations`** map a duty to its **controls**. Every control form except `external` resolves against pack content, and a reference that doesn't resolve is a validation error, so an obligation can't be discharged by naming something that doesn't exist. `validator` points at a guardrail by its new optional `id`.
- **`reviews`** record cadence (an ISO 8601 duration) and an owning team. Completion dates stay out: a date inside a versioned artifact is stale by design. Runtimes that track completions key them by `reviews[].id`.
- **`independent_of`** says a reviewing agent must not share the listed `axes` — `model`, `provider`, `tools`, `prompts`, `accountable_owner` — with whatever produces its input. Naming `accountable_owner` is how a pack expresses **segregation of duties**: the runtime compares two owners it resolved itself rather than trusting a label. There is deliberately no duty field on tools; the stronger separation is two packs, with two digests, two owners and two admission decisions.

Open values take terms from the [well-known prefixes](./schema-guide#well-known-prefixes-v180), mostly the W3C Data Privacy Vocabulary, plus two small minted namespaces (`hipaa:`, `pp:`). Any other scheme works by declaring its prefix in `vocabularies`, and shapes the schema doesn't have go in `extensions` — which v1.8.0 also adds to every definition where a runtime policy makes a decision. See [Policy annotation](./schema-guide#policy-annotation-v180) and [How to Declare Governance](/docs/guides/add-governance).

## Deployment Benefits

The pack structure provides significant operational advantages:

### Single-File Deployment

Everything needed to run your AI system is in one JSON file. No missing dependencies, no scattered configuration files, no "it works on my machine" problems.

### Environment Portability

The same pack works across:

- **Development environments** (local testing)
- **Staging systems** (integration testing)
- **Production deployments** (live systems)
- **Different cloud providers** (AWS, Azure, GCP)
- **Various AI platforms** (OpenAI, Anthropic, local models)

### Atomic Updates

Deploy new versions atomically—either the entire pack updates successfully, or it doesn't. No partial updates that leave your system in an inconsistent state.

### Configuration as Code

PromptPacks enable GitOps workflows:

- **Version Control**: Track all changes through Git
- **Code Reviews**: Review prompt changes like code changes
- **Automated Testing**: CI/CD pipelines can test pack functionality
- **Rollback**: Instantly revert to previous pack versions

## Design Principles

The pack structure follows key principles that make conversational AI more manageable:

**Modularity**: Break complex AI behavior into focused, manageable pieces
**Composability**: Combine specialized prompts with shared resources efficiently  
**Portability**: Work consistently across different environments and providers
**Observability**: Built-in testing and performance tracking for continuous improvement
**Maintainability**: Clear separation makes updates safer and easier

This architecture scales from simple single-prompt packs to complex systems with dozens of specialized prompts, all while maintaining clarity and avoiding duplication.
