Skip to main content
Beta
Schema design patterns shown here use the new /documents/sections and /documents/templates API. See Create a Section for the full endpoint mechanics.

Why schema is the headline feature

outputSchema on a section does two things at once:
  1. Declares the shape of what the LLM is allowed to emit — a string, a number, a structured object with named fields, an array of objects, and so on. The model is steered to fit this shape.
  2. Drives the rendering of that output via format strings (fieldFormat, itemFormat) so the rendered Markdown/text matches your downstream consumer — an EHR field, a structured pipeline, a free-text note block.
Schema descriptions, enums, patterns, defaults, and min/max constraints are all part of the prompt the model sees. Use them as guidance, not just validation.
Schema design is iterative. Start narrow (string + a tight contentPrompt), then widen the schema as you discover repeatable structure in the outputs.

Node types at a glance

outputSchema is one of five node types, discriminated by type. The table lists each type and the fields you can set; bold fields are required, the rest are optional. Important: the schema for an array’s items can itself be any node — including another array or object. That’s the lever for everything below.

Field reference — what each option does

Most fields are technically optional, but description is strongly recommended on every node — it doubles as a prompt to steer the LLM, not just metadata.

Common to all node types

String-specific

Number-specific

Boolean-specific

Array-specific

Object-specific


Composing schemas — quick reference

Where schema interacts with the rest of the prompt

The model receives, in priority order:
  1. Section instructionscontentPrompt, writingStylePrompt, miscPrompt.
  2. Template-level instructionsinstructions.prompt on the parent template (when used in a template).
  3. Schema-level guidance — every description, enum, pattern, default, minimum, maximum, minItems, maxItems you supply on the outputSchema.
Schema-level guidance is where you encode per-field rules that vary inside one section (e.g. “this field is ‘Nil’ when denied”, “this field is one of these abbreviations”). Use instructions.contentPrompt for the overall section content/scope; use schema descriptions for field-specific behavior.

Applying a schema to a Corti Standard section

A common pattern is: keep a Corti Standard section’s full prompt machinery (heading, contentPrompt, writingStylePrompt, miscPrompt) but swap its outputSchema for one of the patterns above — e.g. take the curated corti-hpi prompts and emit a structured object for an EHR pipeline. Two ways to do this: In both forms, outputSchema overrides are wholesale — whatever you submit fully replaces the parent’s schema; partial schemas are not merged. When the change is structural (e.g. stringobject/array), consider overriding writingStylePrompt in the same request so the parent’s wording rules don’t conflict with the new shape.

Create a Section

Wrap any of these schemas in a full section create request — name, language, instructions, lifecycle.

Corti Standards

Browse the curated library — many of the patterns above are how Corti standard sections are built.

Worked clinical examples

The patterns below are pulled from real Corti standard sections — adapted so you can copy/paste them into your own POST /documents/sections request, lift specific fields, or reference them as a parent via inheritFromId. Each example shows the full outputSchema (you’d wrap it in the standard name/language/generation.instructions/generation.outputSchema envelope from Create a Section).
Goal: A pre-op screening block that always renders the same fixed subheadings in the same order. When the conversation didn’t touch a topic, render Not discussed. When the patient explicitly denied something, render Nil. Don’t let the model invent in-between phrases like “patient denies anything noteworthy.”Pattern: object with fieldFormat: "{key}\\n{value}\\n" (per-field iteration), fixed fields[], per-field default, and explicit guidance in each field’s description for the negation rule.
outputSchema — fixed subheadings + explicit defaults
What you get (with fieldFormat: "{key}\n{value}\n" — per-field iteration):
The description on each field’s value description carries the negation rule for that field — the model sees both the field-level description(s) and the section-level instructions.contentPrompt. Use field descriptions for field-specific guidance.
Goal: Examination findings where the LLM picks the organ-system label dynamically — only the systems actually examined appear in the output, with a clinically appropriate label for each. No predefined enum.Pattern: array of objects with a organ-system string field (no enum) and a finding string field, joined via fieldFormat: "{organ-system}: {finding}". The model creates as many entries as needed.
outputSchema — dynamic-key array-of-objects
What you get:
Goal: Same shape, but every label must come from a fixed clinical abbreviation set you’ve standardized on for an EHR field.Pattern: identical to 2a, but the organ-system field’s string node carries an enum of allowed values.
outputSchema — enum-constrained dynamic keys
What you get:
When to use which. Use free titles (2a) when the legal label set is open-ended and clinicians need to choose precise body regions. Use enum (2b) when the downstream consumer (EHR field, analytics pipeline) requires a finite, stable set of labels.
Goal: Lab and imaging results where each entry mixes typed measures (numeric values with ranges and units), a status field drawn from a fixed clinical vocabulary, and a free-text finding narrative.Pattern: array of objects with a result string that the model formats consistently (number + unit, or status placeholder for non-numeric studies), plus a typed status enum and a free-text findings field. Numeric typing is preserved per-test through the description instructions; the rendered line stays clean regardless of test type.
outputSchema — mixed-type test results
What you get:
Every row renders cleanly: result is always non-empty, so there’s no double-space gap, and the rendered line works for quantitative labs and imaging studies alike.
Why not separate value and unit numeric fields? It’s tempting to model labs as { value: number, unit: string } for strong typing, but fieldFormat is a static template — when a field’s value is empty or missing, the literal text around it (spaces, parentheses, separators) still renders. An X-ray entry that legitimately has no numeric value would render as Chest X-ray: (normal). — note the double space — because the format template {test}: {value} {unit} ({status}). {findings} substitutes empty strings for value and unit.Collapsing measurement into a single string result field that the model is instructed to format consistently sidesteps this. If you need strict numeric typing for a downstream pipeline (e.g. lab values into analytics), consider:
  • Splitting into two sectionsquantitative-labs (array of { test, value: number, unit, status }) and imaging-studies (array of { study, status, findings }). Each gets a clean, type-appropriate fieldFormat.
  • Post-processing the rendered string client-side to collapse runs of whitespace.
  • Keeping the result string for human-readable rendering and adding a parallel typed field that downstream consumers read, e.g. numericValue: number. The numericValue field can be omitted from the fieldFormat rendering entirely and only consumed via document.structuredDocument (see Guided Synthesis — response shape).
Goal: A prescription section where every line is prefixed with Rp., and a separate dictation block where every utterance begins with a standard “Pt reports:” header.Pattern: custom itemFormat on the array. The {item} placeholder is the rendered item; everything else around it is literal text.
outputSchema — prescriptions with Rp. prefix
What you get:
outputSchema — dictation with fixed prefix per item
The same {key}/{value} levers exist on objects via fieldFormat (e.g. "**{key}**: {value}") for Markdown-bold headings, or "## {key}\n{value}" for full Markdown headings.
Goal: A plan section that emits literal placeholder strings — like {treatment_shared_motherhood_ivf} — for an EHR system to substitute downstream. The LLM must not generate or paraphrase these; they’re scaffolding for the EHR, not content.Pattern: bake the placeholders directly into fieldFormat as literal text, escaping the curly braces. fieldFormat parses {fieldKey} as a variable substitution; doubled {{ and }} are escapes that render as a single literal { and } in the output. Any clinician-authored content goes into a normal field that is substituted.
Format-string brace escaping. In fieldFormat:
  • A single {fieldKey} substitutes the value of that field.
  • A doubled {{ renders as a literal { in the output; }} renders as a literal }.
So to emit literal {treatment_var} in the output (single braces around a placeholder name), write {{treatment_var}} in the format string — that’s {{ (literal {) + treatment_var (literal text, no substitution because there’s no matching field) + }} (literal }).To emit literal {<value-of-field_1>} (literal braces around a substituted value), write {{{field_1}}} — that’s {{ + {field_1} + }}.To emit double-brace placeholders like {{treatment_var}} literally (Mustache/Handlebars style), each output brace needs its own escape: write {{{{treatment_var}}}} in the format string.
outputSchema — EHR placeholders via escaped literal braces
What you get:
The clinician-generated plan text fills {plan_notes}; the EHR placeholders pass through as literal text because their braces are escaped. No extra fields and no enum tricks needed.
If your EHR expects double-brace placeholders ({{var}} literal in the output, e.g. Mustache/Handlebars conventions), each delimiter needs its own escape. The format string fragment for that line becomes {{{{treatment_shared_motherhood_ivf}}}} — four braces on each side. Verbose but valid.
Goal: Inside one section, each subheader (field) follows a different writing style — telegraphic for one, flowing prose for another, comma-separated short phrases for a third — without splitting into multiple sections.Pattern: the section’s instructions.writingStylePrompt sets the global default; each field’s description carries the local style rule for that subheader. The model reads both and applies the field-level rule where the two disagree.
outputSchema — per-field writing styles
What you get:
When per-field description is enough vs. when to split into multiple sections. Use this pattern when the styles are contrasts within a coherent section that always renders as one block (e.g. a pain assessment). When the subheaders are large enough to be reused independently across templates — or have meaningfully different contentPrompt rules — it’s cleaner to promote each into its own section in the template instead.
Goal: An occupational therapy block with fixed ICF-aligned subcategoriesSelf-care, Mobility, Communication, Domestic life, Interpersonal interactions — each producing free text. Subcategories always render in the same order; subcategories not covered in the source still appear with an explicit “Not assessed” placeholder so downstream consumers can rely on the structure.Pattern: object + fieldFormat: "{key}\\n{value}\\n" (per-field iteration) + fixed fields[] with a per-field default of "Not assessed". This is the OT-clinic equivalent of the pre-op screening pattern in Example 1.
outputSchema — OT Activity & Participation block
What you get:
The “Domestic life” subheader still renders even though the source material didn’t cover it — because the field’s default is "Not assessed". Downstream consumers see a stable structure across encounters.
Goal: Output a clinical letter (or any structured note) where standard sentence templates wrap model-generated content. The connective tissue — salutations, framing sentences, closing — is verbatim; only the clinical content varies between encounters.Pattern: object + fieldFormat with full sentences containing {field} placeholders. The fixed phrases are just literal text in the format string; each {field} is substituted with model-generated content from fields[].
outputSchema — standard phrasing scaffold
What you get:
Compare with Example 5. In Example 5 the {{var}} text is literal in the output — the model never touches it. In Example 8, every {field} is substituted — the literal text is just the connective sentences between the substitutions. Same fieldFormat mechanism, opposite intent for the placeholders.
Goal: A Plan section where each plan item carries a top-level marker (bullet by default — see the numbering caveat below) and 0 or more indented sub-bullets with dosing, follow-up timing, contingencies or anything else worth itemising under that step.Pattern: outer array with a custom itemFormat containing the {item} placeholder. Each item is an object with a summary string field plus a nested details array whose own itemFormat includes a leading indent + bullet marker (e.g. " - {item}\n"). Compose summary + details via fieldFormat. The outer array applies its itemFormat once per top-level entry; the inner array’s itemFormat controls the indented bullet rendering.
outputSchema — plan with nested bullet details
What you get:
How it works:
  • The outer array.itemFormat: "* {item}\n" puts an asterisk (or any literal marker you choose — -, , 1.) in front of each top-level item.
  • The inner array.itemFormat: " - {item}\n" is the key lever: the two leading spaces indent each bullet under its parent, and - is the bullet marker. Adjust spacing for deeper nesting (e.g. " * {item}\n" for a four-space-indented sub-sub-list).
  • The object’s fieldFormat: "{summary}\n{details}" concatenates the summary line with the sub-bullets directly underneath, preserving the visual nesting.
  • If a plan item has no sub-bullets, the details array renders as empty and the entry collapses to just the summary line (item 3 above).
Auto-incrementing numbered lists (1., 2., 3., …) cannot be produced via itemFormat alone. The format string is applied per item with no built-in counter — every item would get the same literal prefix (e.g. "1. {item}" would render "1." in front of every entry).If you need true sequential numbering, change the outer shape to a single string and instruct the model to emit the numbering itself:
The trade-off: you lose the typed array shape (and the details.structuredDocument you’d get from array of object). Keep the array shape if you can live with bullets, switch to string + prompt if the numbering is mandatory.
The same indented-bullet trick scales further within the array shape. To get a third level (outer marker → bullet → sub-bullet), nest another array inside details.items whose own itemFormat uses more leading spaces — e.g. " · {item}\n". The model handles arbitrary nesting; readability for the human reader is usually the constraint.
Goal: A Diagnoses (problem list) section where each diagnosis is rendered with a top-level marker and an optional ICD/SNOMED code, and each diagnosis carries indented sub-bullets describing the clinical findings, reasoning, status, and management pertaining to it. Same structural pattern as Example 9; the field descriptions and the kind of content the LLM is steered toward are diagnosis-focused.Pattern: outer array with a custom itemFormat (e.g. "* {item}\n"), inner items are objects with a diagnosis summary string and a nested details array. The details.itemFormat uses the leading-indent + bullet marker trick to render each sub-point indented under its diagnosis.
outputSchema — diagnoses with indented details
What you get:
How it differs from Example 9:
  • Structurally identical schema shape (outer array + object item with summary + nested details array). The pattern is reusable.
  • The field descriptions are diagnosis-specific: diagnosis prompts for label + optional code; details prompts for findings, reasoning, status, management.
  • Same graceful-collapse behavior — if a diagnosis only has a name (and the source material doesn’t support sub-bullets), the entry renders as just the diagnosis line (item 4 above).
Pair this schema with a writingStylePrompt like “Bullet text is telegraphic; one clinical point per bullet. Do not repeat the diagnosis name inside the bullets.” to keep the indented bullets tight and avoid the model restating the diagnosis label on every line.
Need auto-numbered diagnoses (1., 2., 3., …)? Same caveat as Example 9: itemFormat has no built-in counter, so use string + a contentPrompt that instructs the model to emit numbered lines with two-space-indented sub-bullets. Trade-off: you lose the typed array-of-objects shape and any structuredDocument benefits.
Goal: The section emits exactly one value from a closed list based on cues in the source material. No free text, no explanation — just the categorical value. Useful for EHR picker fields, analytics dashboards, routing logic, or any downstream consumer that needs a stable enum value rather than narrative content.Pattern: string + enum (closed value set) + a rich description that teaches the model the keyword-to-value mapping. Add default for the “no signal in the source” case so the field always renders something deterministic.
outputSchema — encounter disposition classifier
What you get (for a transcript where the clinician says “…let’s send her home with the antibiotic course and a follow-up in two weeks”):
Just the bare enum value, nothing else. Downstream consumers can map it to a UI dropdown selection, an EHR field, a routing decision, or a metric label without any parsing.How it works:
  • enum is the hard constraint. The model can only emit one of the listed values — "admit", "discharge", "transfer", "consult-pending", "left-ama", or "undetermined". Free text never leaks through.
  • description is where the classification logic lives. Spell out which input keywords or phrases map to each enum value. The model reads this alongside the section’s instructions.contentPrompt when picking the output.
  • default covers the empty-state case — if the source material has nothing to support a disposition decision, the field renders "undetermined" deterministically. (Without a default, the model might still pick a value or return an empty string. With it, your downstream consumer always sees a known value.)
Pair this schema with a tight writingStylePrompt like “Output the enum value only. No quotation marks, no surrounding sentences, no explanations.” if you notice the model occasionally adding narration. The schema alone usually suffices, but the extra instruction is cheap insurance for a strict categorical output.
Other clinical use cases that fit this pattern: triage acuity (ESI levels 1–5), pain severity (mild / moderate / severe), tobacco status (never / former / current), pregnancy status, fall-risk class, cancer stage, AMS level, NYHA functional class, audit/consent yes-no-unknown checks. Each one is a string + enum + descriptive keyword rules. For numeric scoring (Apgar, GCS, pain 0–10), use number + minimum/maximum instead — see Example 3 patterns.