> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corti.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipe 4 — outputSchema descriptions: per-node prompting

> Use outputSchema node descriptions as targeted, per-node free-form prompts — the second steering surface alongside contentPrompt and writingStylePrompt.

<Badge className="accent-badge" shape="rounded">Beta</Badge>

← Back to [Prompting & outputSchema Cookbook](/textgen/prompting-cookbook)

## What `description` is for

`outputSchema` is mostly typed constraints — `type`, `enum`, `pattern`, `minimum`/`maximum`, `minItems`/`maxItems` — but every node also accepts a free-form **`description`**. That's your second prompting surface, after `instructions`. It runs lower in the [prompt hierarchy](/textgen/section-schemas#where-schema-interacts-with-the-rest-of-the-prompt) but is uniquely **per-node**: each `description` only steers the field or item it's attached to.

Use `description` when guidance is **scoped to one slot** in the output and would be wasteful (or confusing) if you put it in section-wide prompts.

<Tip>
  For the full reference on schema node types, `enum`/`pattern`/`itemFormat`/`fieldFormat`, and worked clinical examples, see [Section Schemas](/textgen/section-schemas). This recipe focuses only on what to write inside `description`.
</Tip>

## Where `description` lives

Every node in an `outputSchema` accepts a `description`. The most common places:

* **Top-level node** — the whole section's output. Often a one-sentence summary of what this section produces; sometimes carries structural directives.
* **`items` (on an array)** — describes one entry in the array. Repeated guidance per item.
* **Per-field (on `object.fields[]`)** — the targeted slot. This is the most useful place.
* **Nested field's `value` node** — for typed leaves that need their own per-field guidance.

## Three jobs `description` does well

### 1. Scope guidance per field

When an `object` has many fields, a one-line `description` per field tells the model exactly what belongs in that slot — with precision and clarity inside the schema.

From `corti-objective`:

```json theme={null}
"fields": [
  { "key": "General",
    "description": "General condition and overall clinical impression, e.g. level of consciousness, general/nutritional/care status, pain, mobility.",
    "value": { "type": "string" } },
  { "key": "Head & Neck",
    "description": "Findings on examination of the head and neck, e.g. skull, eyes, nose, teeth, tongue, pharynx/tonsils, ears, thyroid, jugular veins.",
    "value": { "type": "string" } },
  { "key": "Heart",
    "description": "Cardiac auscultation: heart sounds, rhythm, murmurs.",
    "value": { "type": "string" } }
]
```

Each field gets its own scope. `contentPrompt` stays high-level (*"Include direct findings from the physical examination …"*); the schema descriptions carve up the body-region anatomy.

### 2. Layered guidance per schema node

Let's take the desired output format for a Past Medical History section, e.g.

```
2015: Appendectomy (acute)
2021: COVID (mild symptoms)
```

Typically, you would prompt for this in a couple paragraphs. And while you can opt to do so, we recommend you take advantage of the schema for increased robustness and the ability to utilize structured outputs.

From `corti-past-medical-history`:

```json theme={null}
"outputSchema": {
      "type": "array",
      "description": "Past medical or surgical conditions, one condition per line. Each item is a short clinical note; when a time reference is known, the item begins with the time reference followed by a colon and the condition.",
      "itemFormat": "{item}\n",
      "items": {
        "type": "object",
        "description": "One past medical history entry, with an optional time prefix and the condition or event.",
        "fieldFormat": "{prefix}{content}",
        "fields": [
          {
            "key": "prefix",
            "description": "Time prefix with trailing colon and space, e.g. '2019: ' or 'Age 14: '. Empty string when no time reference is documented in the source.",
            "value": {
              "type": "string"
            }
          },
          {
            "key": "content",
            "description": "The condition, procedure, diagnosis, or event description.",
            "value": {
              "type": "string"
            }
          }
        ]
      }
    }
```

Each `description` in the hierarchy steers exactly one layer of the output:

* **`array.description`** — the section-wide goal: a list of past conditions, one per line, with optional time prefixes. The model reads this once to understand the section's overall job.
* **`items.description`** (the `object` node) — what **one entry** in the list represents: a past medical history entry composed of an optional time prefix and a condition. This is the *anatomy of a single item* — every item the model produces is shaped by this guidance.
* **`prefix.description`** — the targeted rule for **just the time-prefix slot**: format with trailing colon and space; empty string when no time reference is documented. The model never has to remember this rule while reasoning about the condition itself.
* **`content.description`** — the targeted rule for **just the condition slot**: the actual diagnosis, procedure, or event description.

`fieldFormat: "{prefix}{content}"` then literally renders the two filled slots into the per-item string.

**Why this is more robust than one big paragraph prompt:**

* **Each rule is local.** The model isn't asked to remember the prefix-formatting rule *while* extracting the diagnosis. Each `description` sits next to the field it governs, so the relevant guidance is always in the model's immediate context for that slot.
* **The output renders itself.** `fieldFormat` does the literal layout — once the model fills the slots correctly, the colon-and-space, the line breaks, the ordering are guaranteed. You're not relying on the model to remember formatting rules at generation time.
* **You get structured output.** The response is an array of `{prefix, content}` objects, not a free-text blob. Downstream systems can parse, validate, sort, or post-process each entry — no fragile regex needed to recover structure that was lost when the model rendered the prose.
* **Typed constraints compose for free.** Add `enum` to `prefix.value` for a closed vocabulary of time formats, `minItems`/`maxItems` on the array, `pattern` for a strict prefix regex — each lives next to the description it qualifies and gets *enforced*, not just steered.

One free-form paragraph prompt has to carry every one of those rules at once, and there's no guarantee the model honours all of them in every output. The schema localises each rule and enforces the format programmatically.

### 3. Per-field fallback conditionals

From `corti-allergies`:

```json theme={null}
{ "key": "reaction",
  "description": "Reaction detail with a leading ': ', e.g. ': rash' or ': anaphylaxis'. For a documented allergen with no described reaction, use ': not specified'. Leave empty for no known allergies (NKDA).",
  "value": { "type": "string" } }
```

Three different fallback states (*reaction known* / *allergen but no reaction* / *NKDA*) — all encoded as one field-scoped `description` as conditional `default`s are not supported.

### 4. Explain how to *use* enum options

`enum` enforces what's allowed; `description` explains *when* to pick which value. From `corti-mental-status-exam`:

```json theme={null}
{ "key": "stdLabel",
  "description": "Standard MSE domain. Choose from the predefined list, or empty string if the observation doesn't fit a standard domain (in which case populate customLabel).",
  "value": { "type": "string", "enum": ["", "Appearance", "Behavior", "Speech", "Mood", "Affect", "Thought Process", "Thought Content", "Perception", "Cognition", "Insight and Judgment"] } },
{ "key": "customLabel",
  "description": "Custom domain label, used only when stdLabel is empty string. Empty string when a standard domain is selected.",
  "value": { "type": "string" } }
```

The two descriptions together teach a contract: *pick a standard label, OR pick the empty-string sentinel and populate `customLabel` instead.* `enum` alone can't express that interplay; the descriptions carry it.

## Anti-patterns

* **Don't restate `contentPrompt` scope here.** Section-wide *"what to include"* belongs in [Recipe 1](/textgen/prompting-cookbook/recipe-1-content-prompt). Schema `description` is for *this slot* only.
* **Don't restate `writingStylePrompt` voice here.** Section-wide tone/register belongs in [Recipe 2](/textgen/prompting-cookbook/recipe-2-writing-style). If every field's `description` says *"use concise medical language"*, move it up.
* **Don't write a description longer than the rule it enforces.** A leaf-field `description` is usually one sentence. If you're writing three, the field probably needs to be split, or the rule belongs in `miscPrompt`.
* **Don't restate what `enum`/`pattern` already enforces.** *"Use one of: PO, IV, IM …"* in `description` is redundant when those are the `enum` values. Use `description` to explain *why* / *when*, not *what*.
* **Don't leave descriptions empty when the field is non-obvious.** Empty descriptions on cryptic field keys (`prefix`, `trailer`, `stdLabel`) make the model guess. The corpus consistently populates them.

## Related

* [Section Schemas](/textgen/section-schemas) — schema mechanics (node types, `enum`, `pattern`, `itemFormat`, `fieldFormat`) and worked clinical examples.
* [Recipe 1 — contentPrompt](/textgen/prompting-cookbook/recipe-1-content-prompt) — section-wide *what to include*.
* [Recipe 2 — writingStylePrompt](/textgen/prompting-cookbook/recipe-2-writing-style) — section-wide voice and shape.
* [Recipe 3 — miscPrompt](/textgen/prompting-cookbook/recipe-3-misc-prompt) — fall-back guardrails that span the whole section.
