---
title: Pack draft format
description: The felspar.pack-draft JSON format that check_pack and propose_pack accept - purpose, subjects, questions, outcomes, escalations and test cases - with every field and limit.
sidebar:
  order: 4
---

A pack draft describes one job: what it is for and when to use it, what each case is about, the questions an agent answers from evidence, the outcomes, the rules that pick an outcome, where undecided cases escalate, and test cases. `check_pack` and `propose_pack` take it, together with the `source_object_id` of the document it quotes. Felspar locates every quote and builds the full proposal from it, so nobody writes fingerprints, passages or classifier settings by hand.

The same guide is available to any assistant through the `get_pack_guide` tool.

## Format versions

| `schema` | Adds | Status |
| --- | --- | --- |
| `felspar.pack-draft/v2` | A required `purpose`, plus `useWhen`, `notFor`, `owner`, `subjects`, `subject` and `escalations` | Current |
| `felspar.pack-draft/v1` | Number and date questions with quoted thresholds, and scoring | Accepted; numbers, dates and scoring are in preview |
| `felspar.pack-draft/v0` | Answer-choice questions, outcomes, rules and test cases | Accepted |

Every v0 draft is also a valid v1 draft, and v2 is v1 with a required `purpose`. Subjects, escalations and the job fields are optional before v2. A draft that doesn't use them builds exactly the same release as before.

## Shape

A fictional support-desk policy, drafted as v2:

```json
{
  "schema": "felspar.pack-draft/v2",
  "job": "triage-ticket",
  "purpose": "<quote: why the job exists and what it resolves>",
  "useWhen": ["<quote>"],
  "notFor": ["<quote>"],
  "owner": "<quote: the accountable role>",
  "subjects": [
    { "id": "ticket", "definition": "<quote>", "identifier": "help desk ticket number", "example": "48213",
      "relationships": [
        { "name": "customer", "subject": "customer", "cardinality": "one", "definition": "<quote>" }
      ] },
    { "id": "customer", "definition": "<quote>", "identifier": "account ID", "example": "cus-9f2" }
  ],
  "subject": "ticket",
  "escalations": [
    { "id": "human-review", "quote": "<quote>" },
    { "id": "incident-review", "quote": "<quote>" }
  ],
  "evidenceKinds": ["ticket", "account"],
  "maxEvidenceAgeDays": 30,
  "questions": [
    {
      "key": "topic",
      "meaning": "<quote: the question>",
      "answers": [
        { "value": "charge", "meaning": "<quote>", "inclusions": ["<quote>"], "exclusions": ["<quote>"], "examples": ["<quote>"] },
        { "value": "failure", "meaning": "<quote>", "examples": ["<quote>"] },
        { "value": "other", "meaning": "<quote>", "noneOfThese": true }
      ],
      "whereToLook": ["<quote>"]
    },
    {
      "key": "premium",
      "about": "customer",
      "meaning": "<quote>",
      "answers": [
        { "value": "yes", "meaning": "<quote>" },
        { "value": "no", "meaning": "<quote>" }
      ]
    }
  ],
  "outcomes": [
    { "id": "billing", "nextStep": "<quote: what to do next>" },
    { "id": "on-call", "nextStep": "<quote>", "escalate": "incident-review" },
    { "id": "general", "nextStep": "<quote>" }
  ],
  "whenUnresolved": { "quote": "<quote: what to do when nothing is decided>", "escalate": "human-review" },
  "attempts": { "max": 2, "escalate": "human-review" },
  "rules": [
    { "id": "charge", "meaning": "<quote>", "when": { "topic": "charge" }, "outcome": "billing" },
    { "id": "failure", "meaning": "<quote>", "when": { "topic": "failure" }, "outcome": "on-call" },
    { "id": "other", "meaning": "<quote>", "when": { "topic": "other" }, "outcome": "general" }
  ],
  "cases": [
    { "id": "invoice", "why": "<where the document says so>", "subject": { "id": "48213" },
      "facts": { "topic": "charge", "premium": "no" }, "expect": { "outcome": "billing", "rule": "charge" } },
    { "id": "outage", "why": "<where the document says so>",
      "facts": { "topic": "failure", "premium": "yes" },
      "expect": { "outcome": "on-call", "rule": "failure", "escalation": "incident-review" } }
  ]
}
```

## Fields

### The job

| Field | Required | Rules |
| --- | --- | --- |
| `schema` | Yes | `"felspar.pack-draft/v2"` (or `v1`, `v0`) |
| `job` | Yes | The job ID agents use, such as `triage-ticket` or `review-nda` |
| `purpose` | Yes in v2 | Quote: why the job exists and what it resolves |
| `useWhen`, `notFor` | No | 1 to 8 quotes each: when the document says to use the job, and when not |
| `owner` | No | Quote: the accountable role, only if the document names one |
| `evidenceKinds` | Yes | 1 to 8 kinds of evidence that count, such as `ticket`, `contract` or `company-site`. These become the source classes agents send. |
| `maxEvidenceAgeDays` | Yes | 1 to 3,650. How old evidence may be. |

Agents see `purpose`, `useWhen`, `notFor` and the subject first, in `list_jobs` and `get_job_requirements`, and pick a job by them.

### Subjects

| Field | Required | Rules |
| --- | --- | --- |
| `subjects` | No | 1 to 16 subject types: each kind of record the document talks about |
| `subjects[].id` | Yes | The record's name in the document, such as `ticket` or `contract` |
| `subjects[].definition` | Yes | Quote: what the record is |
| `subjects[].identifier` | For `provided` | How its ID is written, such as "the contract ID in the matter system". Needed when the job's cases use the record's own ID, and for any subject a relationship points to. |
| `subjects[].example` | With `identifier` | One ID, written as Felspar keeps it (lowercase for `case-insensitive`) |
| `subjects[].match` | No | `exact` (default) or `case-insensitive` |
| `subjects[].identity` | No | How a case gets its ID: `provided`, `fields`, `content` or `issued` ([below](#identity-modes)). Without it, a subject with an `identifier` is `provided` and one without is `issued`. |
| `subjects[].relationships` | No | Up to 16, each `{ name, subject, cardinality, required?, definition }`: `subject` is another declared subject, `cardinality` is `one` or `many`, `required` is `true` when the document says a decision needs that record, and `definition` is a quote |
| `subject` | No | The subject type this job decides about |
| `questions[].about` | No | The relationship whose record a question's evidence comes from |
| `cases[].subject` | No | `{ id }`: decide this test case as that record |

See [Subjects and escalations](/concepts/subjects-and-escalations) for how agents send related records and what results carry.

### Questions and answers

| Field | Required | Rules |
| --- | --- | --- |
| `questions` | Yes | 1 to 64 |
| `questions[].key` | Yes | The question's ID |
| `questions[].meaning` | Yes | Quote: the question |
| `questions[].answers` | Yes, for answer-choice questions | 2 to 64 answers |
| `answers[].value` | Yes | The answer's ID |
| `answers[].meaning` | Yes | Quote: what the answer means |
| `answers[].inclusions`, `exclusions`, `examples`, `edgeCases` | No | Up to 8 quotes each |
| `answers[].noneOfThese` | No | `true` on at most one answer per question: the answer for text that clearly says something outside the list |
| `questions[].askOnlyWhen` | No | Conditions under which the question applies |
| `questions[].whereToLook` | No | Up to 8 quotes: where the policy says to look |

### Outcomes, escalations and rules

| Field | Required | Rules |
| --- | --- | --- |
| `outcomes` | Yes | 1 to 64, each `{ id, nextStep, escalate? }` with `nextStep` a quote and `escalate` an escalation ID |
| `escalations` | No | 1 to 16, each `{ id, quote }`: a hand-off the document names, such as `legal-review` |
| `whenUnresolved` | Yes | Quote: what an agent does when nothing is decided. Or `{ quote, escalate }` to name the hand-off too. |
| `attempts.max` | No | 1 to 10, default 3: how many decisions an agent may ask for on one undecided case |
| `attempts.escalate` | No | The escalation once a case has used its attempts (otherwise `whenUnresolved`'s) |
| `rules` | Yes | 1 to 256, in order: the first that matches decides |
| `rules[].when` | Yes | Question keys mapped to one answer, or a list of 2 to 64 answers meaning "any of these" |

### Test cases

| Field | Required | Rules |
| --- | --- | --- |
| `cases` | Yes | 1 to 200 test cases |
| `cases[].facts` | Yes | Question keys mapped to answers |
| `cases[].expect` | Yes | `{ outcome, rule, escalation? }`: the outcome the policy says, the rule that should decide it, and the escalation the document says it hands off to |

Identifiers use lowercase letters, digits, `-` and `_`, starting with a letter or digit. Quotes are at most 4,000 characters. Subject examples and case IDs use letters, digits and `. _ : @ -`, up to 200 characters.

## Rules Felspar enforces

- **Meaning comes only from the document.** Every `meaning`, `nextStep`, `whenUnresolved`, `purpose`, `useWhen`, `notFor`, `owner`, subject and relationship `definition`, escalation `quote`, inclusion, exclusion, example and where-to-look is an exact quote from `read_source`, copied character for character (keep escapes such as `\_`). A quote must appear exactly once in the document. An answer's quote is first looked for, as whole words, inside its question's quote, so an option such as "yes" can be quoted where the question lists it.
- **Rules are in order.** The first rule that matches decides.
- **Conditional questions.** A question with `askOnlyWhen` is asked only when those answers hold. Every rule that uses it must repeat that exact condition.
- **Tests come from the document.** Take each expectation from the policy, never from what the draft returns.
- **Don't decide what the document leaves open.** Report contradictions, missing values and undecided cases to the owner as gaps, with both quotes. If the document doesn't say how a record is identified, which records a decision needs, or who reviews a held case, ask the owner; never invent one.
- **Escalations name a kind of review,** such as `human-review` or `compliance-review`, never a tool, channel or person. The company's own code decides what each one does.
- **Quote budget.** One question's quotes may come from at most 16 separate blocks of the document; quotes in the same list or table count once.

## Findings

Beyond errors at a path, `check_pack` and `propose_pack` return completeness findings:

- **Blocking:** an answer without a definition; a multiple-choice question without a "none of these" answer that a rule routes; a threshold without test cases on both sides of its edge. `propose_pack` refuses while any remain.
- **Warnings:** answers without inclusions or examples; answers without exclusions; combinations of answers no rule decides; an evidence age the document doesn't state; answers that describe an absence; "unknown" options; questions, outcomes or rules nothing uses or tests; escalations declared while `whenUnresolved` names none, so an undecided case has nowhere to go.

Some findings carry `candidates`: lines in the document that mention the answer and might supply the missing quote.

When the draft revises a job that already has a release, `check_pack` and `propose_pack` also return `breaking`: each escalation ID, subject type or relationship name the draft removes, and a changed job subject, with the number of agents that run the job. See [Review and backtest](/guides/review-and-backtest#breaking-changes).

## Numbers, dates and scoring

These need `felspar.pack-draft/v1` or v2 and are in preview. `get_pack_guide` has the full rules.

- **Number questions** give a `type: "number"`, a `unit` (a currency code, `percent`, or the noun a count is of, with `unitWords`), and a quoted `meaning`. **Date questions** give `type: "date"`. Felspar reads both exactly from the agent's excerpt, with no model, so they need no answers.
- **Thresholds** in a rule name their bound and quote where the document states it: `{ "employees": { "lt": 500, "quote": "fewer than 500 employees" } }`. A date bound can be a date, another date question plus days, or the decision date plus days. Every threshold needs test cases on both sides of its edge.
- **Scoring** adds up quoted points per criterion; rules then become score bands (`score: { gte, quote }`) and gates (`gate: true`), and test cases give the expected total.

## Output checks

When the document says what a written output (a reply, an email) must or must not say, `outputChecks` names the output, the outcomes that produce it, and its checks: exact wording it must contain (`require`) or must not (`prohibit`), the case's own answer it must state (`states`), yes/no questions asked of each paragraph (`ask`), and the approved `context` an agent may write from. Each check quotes the document. After release, a decided result lists the outputs its outcome asks for, and agents check what they wrote with [`check_output`](/reference/mcp-tools#check_output).

## Identity modes

A subject's `identity` says how a case gets its ID when the record has none of its own:

| `identity` | Means | Checked |
| --- | --- | --- |
| `{ "mode": "provided" }` | The record's own ID | The subject needs `identifier` and `example` |
| `{ "mode": "fields", "fields": [{ "name", "identifier", "example", "match"? }] }` | Named values the job collects, such as an email address and a requisition number, normalized by each field's `match` (default `exact`) and hashed in name order | 1 to 8 fields with unique names; each `example` written as its `match` keeps it |
| `{ "mode": "content", "question": "<key>" }` | The text one question judges is the case | `question` is a question key in the job |
| `{ "mode": "issued" }` | Felspar issues an ID on the first decision | Nothing else |

Without `identity`, a subject with an `identifier` is `provided` and one without is `issued`. A v2 job without a subject is `issued`; a v0 or v1 job without one keeps the caller's own ID. A subject that a relationship points to is always sent by its own ID, so it must be `provided`, with an `identifier`. See [Subjects and escalations](/concepts/subjects-and-escalations#identity-modes) for what agents send and what results carry.
