Skip to main content

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.

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.
typeUse whenRequiredOptional
stringFree prose, single labels, fixed phrasestypedescription, default, enum, pattern
numberMeasurements, counts, scorestypedescription, default, enum, minimum, maximum
booleanYes/no facts (e.g. “fasting?”)typedescription, default
arrayLists of any node typetype, itemsdescription, itemFormat, minItems, maxItems
objectStructured blocks with named fieldstypedescription, fields[], fieldFormat
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

FieldPurpose
typeThe discriminator. One of string, number, boolean, array, object. Always required.
descriptionPrompt text the model sees when generating this part of the output. Optional but strongly recommended — this is your main lever to clarify what this specific node should contain, in addition to the section’s instructions.contentPrompt.

String-specific

FieldPurpose
defaultFallback string if the model has nothing to emit for this node.
enumA closed set of allowed values. The model must pick one of these strings.
patternA regular expression the output should match. The model is steered toward emitting a string that conforms (e.g. ^\d{2,3}/\d{2,3}$ for a blood-pressure value). Not a hard validator — pair with enum/description for stricter control.

Number-specific

FieldPurpose
defaultFallback numeric value when nothing applies.
enumClosed set of allowed numeric values.
minimum / maximumPlausible range bounds. Acts as a prompt anchor as well as validation.

Boolean-specific

FieldPurpose
defaultFallback true/false when the source material is silent.

Array-specific

FieldPurpose
items (required)The schema for each element. Can be any node type — string, number, object, even another array.
itemFormatPer-item rendering. bullet (default), numbered, plain, or a custom string containing {item} (e.g. "Rp. {item}").
minItems / maxItemsBounds on the number of items the model may emit.

Object-specific

FieldPurpose
fields[]The fixed set of fields the object contains. Each entry requires key, description, and value (any node type).
fieldFormatSingle unified format string controlling how the object is rendered. Two modes — pick one per object: (a) Per-field iteration with generic {key} and {value} placeholders (e.g. "{key}: {value}\n" for inline subheadings, "{key}\n{value}\n" for block subheadings, "**{key}**: {value}\n" for bold Markdown). The template is applied to every field. (b) Custom layout with specific field-key placeholders that match defined fields[] (e.g. "{test}: {result} ({status})"). The format string is rendered once with all fields substituted.

Composing schemas — quick reference

PatternShapeUse for
stringOne line of proseHPI, Chief Complaint, single-paragraph Assessment
string + enumOne value from a closed setTriage acuity, urgency, disposition
number + minimum/maximumConstrained measureVital values, scores, counts
array of string + itemFormatRepeating short itemsDiagnoses, Medications, Plan items
array of object (free title)Dynamic key/value rowsObjective findings, Diagnostic results, Physical Exam
array of object (enum’d title)Constrained-key rowsReview of Systems with standardized domains
object + fieldFormat per-field iteration ({key} / {value})Fixed-subheading block, optionally with per-field style rulesPre-op screening, OT Activity & Participation, pain assessment, fixed-form SOAP-style blocks
object + fieldFormat custom layout (literal text + {fieldKey} placeholders)Sentence-template scaffoldReferral letter, discharge letter, any note with fixed phrasing around variable content
object + fieldFormat custom layout with escaped {{ }}Layout with literal braces in outputEHR placeholder scaffolds, mixed-type test rows, structured discharge summaries

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
{
  "type": "object",
  "description": "Pre-operative screening block. Render every subheading; never omit a field.",
  "fieldFormat": "{key}\n{value}\n",
  "fields": [
    {
      "key": "Allergies",
      "description": "Allergies and intolerances. If the patient explicitly denied any, output exactly: Nil. If allergies were not discussed at all, output exactly: Not discussed.",
      "value": {
        "type": "string",
        "default": "Not discussed",
        "description": "Use 'Nil' only when the patient explicitly denied allergies. Otherwise 'Not discussed'."
      }
    },
    {
      "key": "Current medications",
      "description": "Current active medications. 'Nil' if patient explicitly denied any. 'Not discussed' if not covered.",
      "value": {
        "type": "string",
        "default": "Not discussed"
      }
    },
    {
      "key": "Fasting status",
      "description": "Whether the patient is fasting and since when. 'Not discussed' if absent from the conversation.",
      "value": {
        "type": "string",
        "default": "Not discussed"
      }
    },
    {
      "key": "Anesthesia history",
      "description": "Any previous anesthesia and complications. 'Nil' if explicitly denied; 'Not discussed' otherwise.",
      "value": {
        "type": "string",
        "default": "Not discussed"
      }
    }
  ]
}
What you get (with fieldFormat: "{key}\n{value}\n" — per-field iteration):
Allergies
Penicillin — anaphylaxis

Current medications
Nil

Fasting status
NPO since 22:00 yesterday

Anesthesia history
Not discussed
The description on each field carries the negation rule for that field — the model sees both the field-level description 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 title string field (no enum) and a content string field, joined via fieldFormat: "{title}: {content}". The model creates as many entries as needed.
outputSchema — dynamic-key array-of-objects
{
  "type": "array",
  "description": "Open-ended set of examination findings. One entry per body region or system, generated only when relevant findings exist.",
  "items": {
    "type": "object",
    "fields": [
      {
        "key": "title",
        "description": "Body-region or system label, e.g. 'Abdomen', 'Right knee', 'Neuro', 'Cardiovascular'. Choose the most clinically appropriate label for the findings in this entry.",
        "value": { "type": "string" }
      },
      {
        "key": "content",
        "description": "Concise objective finding text for that label.",
        "value": { "type": "string" }
      }
    ],
    "fieldFormat": "{title}: {content}"
  },
  "itemFormat": "{item}\n"
}
What you get:
Abdomen: Soft, non-tender, no palpable masses.
Neuro: GCS 15. Cranial nerves intact.
MSK: ROM preserved in all joints; no swelling or deformity.
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 title field’s string node carries an enum of allowed values.
outputSchema — enum-constrained dynamic keys
{
  "type": "array",
  "description": "Review of systems entries. One entry per domain mentioned. Use only the standardized domain abbreviations.",
  "items": {
    "type": "object",
    "fields": [
      {
        "key": "title",
        "description": "Standardized review-of-systems domain label.",
        "value": {
          "type": "string",
          "enum": ["C-P", "GI", "Resp.", "Neuro", "MSK", "ENT", "GU", "Derm", "HEENT", "Endo", "Psych", "Heme", "Immune"]
        }
      },
      {
        "key": "content",
        "description": "Symptoms and explicitly denied symptoms for that domain, stated concisely in clinical language.",
        "value": { "type": "string" }
      }
    ],
    "fieldFormat": "{title}: {content}"
  },
  "itemFormat": "{item}\n"
}
What you get:
C-P: Denies chest pain or palpitations.
Resp.: Mild dry cough for 3 days; no shortness of breath.
GI: Intermittent nausea; no vomiting or diarrhea.
Neuro: Headache for 3 days; no focal deficits.
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
{
  "type": "array",
  "description": "Diagnostic test results discussed in the encounter. One entry per test or imaging study.",
  "items": {
    "type": "object",
    "fields": [
      {
        "key": "test",
        "description": "The name of the test, panel, study or modality (e.g. 'Hemoglobin', 'Chest X-ray', 'Troponin I').",
        "value": { "type": "string" }
      },
      {
        "key": "result",
        "description": "For quantitative tests: numeric value with unit, as stated in the source (e.g. '11.8 g/dL', '0.04 ng/mL'). Do not convert units. For non-quantitative studies (imaging, cultures): use a short placeholder such as 'see findings' or 'pending'. Always non-empty.",
        "value": { "type": "string" }
      },
      {
        "key": "status",
        "description": "Interpretation as stated in the source.",
        "value": {
          "type": "string",
          "enum": ["normal", "low", "high", "abnormal", "pending", "not reported"]
        }
      },
      {
        "key": "findings",
        "description": "Free-text findings, qualifiers, or narrative description from the source. Empty string if the quantitative result alone is sufficient.",
        "value": { "type": "string", "default": "" }
      }
    ],
    "fieldFormat": "{test}: {result} ({status}). {findings}"
  },
  "itemFormat": "{item}\n"
}
What you get:
Hemoglobin: 11.8 g/dL (low). Mildly reduced; no overt anemia.
Chest X-ray: see findings (normal). No acute infiltrate or effusion.
Troponin I: 0.04 ng/mL (normal). Within reference range at 0h and 3h.
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
{
  "type": "array",
  "description": "Prescriptions issued at this visit. One prescription per entry.",
  "items": {
    "type": "string",
    "description": "Prescription line: drug + strength + form, route, frequency, duration."
  },
  "itemFormat": "Rp. {item}"
}
What you get:
Rp. Amoxicillin 500 mg PO three times daily for 7 days
Rp. Ibuprofen 400 mg PO every 6 hours as needed for pain
outputSchema — dictation with fixed prefix per item
{
  "type": "array",
  "description": "Patient-reported statements. One quote or paraphrase per entry.",
  "items": { "type": "string" },
  "itemFormat": "Pt reports: {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
{
  "type": "object",
  "description": "IVF plan block. Clinician narrative goes into {plan_notes}; the remaining lines are EHR placeholders that survive verbatim for downstream substitution.",
  "fieldFormat": "{plan_notes}\n\nShared Motherhood IVF: {{treatment_shared_motherhood_ivf}}\nWith or without PGT-A (if IVF): {{treatment_pgt_a}}\nNumber of embryos to transfer (if IVF/FET): {{treatment_number_of_embryos}}",
  "fields": [
    {
      "key": "plan_notes",
      "description": "Clinician-authored plan narrative for this visit. May be multi-line; do not include the structured EHR placeholder lines — those are emitted separately.",
      "value": { "type": "string", "default": "" }
    }
  ]
}
What you get:
Continue stim protocol. Recheck E2 and follicle count in 48h. Consider OPU end of next week.

Shared Motherhood IVF: {treatment_shared_motherhood_ivf}
With or without PGT-A (if IVF): {treatment_pgt_a}
Number of embryos to transfer (if IVF/FET): {treatment_number_of_embryos}
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
{
  "type": "object",
  "description": "Pain assessment block. Each subheader has its own writing style.",
  "fieldFormat": "{key}\n{value}\n",
  "fields": [
    {
      "key": "Onset & duration",
      "description": "Telegraphic; no narrative. Anchor with explicit dates or durations (e.g. '3 weeks ago, intermittent').",
      "value": { "type": "string" }
    },
    {
      "key": "Pattern of pain",
      "description": "Flowing prose, one or two sentences. Describe the qualitative character (sharp, dull, throbbing, radiating).",
      "value": { "type": "string" }
    },
    {
      "key": "Aggravating / relieving",
      "description": "Comma-separated short phrases, no full sentences (e.g. 'worse with stairs, eased by ibuprofen').",
      "value": { "type": "string" }
    },
    {
      "key": "Treatments tried",
      "description": "Chronological list, one intervention per line, with outcome appended (e.g. 'Ibuprofen 400 mg TID for 1 week — partial relief').",
      "value": { "type": "string" }
    }
  ]
}
What you get:
Onset & duration
3 weeks ago, intermittent.

Pattern of pain
Throbbing pain in the lower back, occasionally radiating to the right leg. Worse in the morning, eases over the course of the day.

Aggravating / relieving
Worse with stairs, eased by ibuprofen, worse after prolonged sitting.

Treatments tried
Paracetamol 500 mg PRN — limited effect
Ibuprofen 400 mg TID for 1 week — partial relief
Physiotherapy, 4 sessions — ongoing
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
{
  "type": "object",
  "description": "Occupational therapy: Activity & Participation block (ICF-aligned). One free-text observation per fixed subcategory.",
  "fieldFormat": "{key}\n{value}\n",
  "fields": [
    {
      "key": "Self-care",
      "description": "Performance in washing, dressing, toileting, eating, medication management. Note both independent activities and where assistance is needed.",
      "value": { "type": "string", "default": "Not assessed" }
    },
    {
      "key": "Mobility",
      "description": "Walking distance, transfers, stairs, use of mobility aids. Include assistance level.",
      "value": { "type": "string", "default": "Not assessed" }
    },
    {
      "key": "Communication",
      "description": "Receptive and expressive communication, comprehension of instructions, any use of alternative communication.",
      "value": { "type": "string", "default": "Not assessed" }
    },
    {
      "key": "Domestic life",
      "description": "Meal preparation, household tasks, shopping, money management. Note level of independence.",
      "value": { "type": "string", "default": "Not assessed" }
    },
    {
      "key": "Interpersonal interactions",
      "description": "Engagement with family, caregivers and social network. Note changes since onset.",
      "value": { "type": "string", "default": "Not assessed" }
    }
  ]
}
What you get:
Self-care
Independent in washing and dressing; requires verbal cueing for medication intake.

Mobility
Walks 50 m with a walker; needs assistance for stairs. Transfers independently bed-to-chair.

Communication
Receptive language intact; mild word-finding difficulty in expressive speech.

Domestic life
Not assessed

Interpersonal interactions
Lives with spouse; reports reduced social engagement since onset.
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
{
  "type": "object",
  "description": "Referral letter scaffold. Fixed phrasing wraps model-generated clinical content.",
  "fieldFormat": "Dear colleague,\n\nThank you for seeing this patient. {chief_complaint}\n\nThe relevant history is as follows: {hpi}\n\nOn examination: {exam_findings}\n\nMy clinical impression is {assessment}. I have started {initiated_treatment}.\n\nI would appreciate your further evaluation and management. Please do not hesitate to contact me with any questions.\n\nKind regards,",
  "fields": [
    {
      "key": "chief_complaint",
      "description": "One sentence stating the reason for referral.",
      "value": { "type": "string" }
    },
    {
      "key": "hpi",
      "description": "Concise history of present illness as flowing prose, 2-4 sentences.",
      "value": { "type": "string" }
    },
    {
      "key": "exam_findings",
      "description": "Key positive findings and pertinent negatives, telegraphic.",
      "value": { "type": "string" }
    },
    {
      "key": "assessment",
      "description": "Provisional diagnosis or working impression, one sentence.",
      "value": { "type": "string" }
    },
    {
      "key": "initiated_treatment",
      "description": "What you have already started (medication, investigation, lifestyle advice). If none, the default phrasing is used.",
      "value": { "type": "string", "default": "no treatment to date" }
    }
  ]
}
What you get:
Dear colleague,

Thank you for seeing this patient. The patient presents with a 6-week history of progressive exertional dyspnea.

The relevant history is as follows: 64-year-old former smoker with known hypertension. Symptoms worsening over the last two weeks with bilateral ankle swelling and orthopnea.

On examination: BP 148/92, HR 88 regular, bibasal crackles, pitting oedema to mid-shin bilaterally.

My clinical impression is decompensated heart failure with preserved ejection fraction. I have started furosemide 40 mg PO daily.

I would appreciate your further evaluation and management. Please do not hesitate to contact me with any questions.

Kind regards,
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.