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

# Disco XML

This page is the complete specification. AI agents connected through the [Disco MCP Connector](/features/mcp) receive the same document as the resource `disco://docs/disco-xml-spec`, so an agent and a person always work from one specification.

Disco XML is the format the Disco API and MCP Connector use for rich content: lesson bodies,
descriptions, and any other field authored in the Disco editor. When you read content you
receive Disco XML. When you create or update content you send Disco XML.

The format is a small, strict XML vocabulary. It is deliberately close to HTML for ordinary
prose, with Disco-specific elements only where HTML has no name for something.

Three things to know before anything else:

1. **It is strict.** Anything not in this document is rejected with an error that says where
   the problem is and, where possible, what you probably meant. Nothing is silently dropped or
   guessed.
2. **It round-trips.** Any content you can create in the Disco editor can be read as Disco
   XML and written back without loss.
3. **Output is deterministic.** Reading the same content always produces the same text, so
   you can diff it and make find-and-replace edits safely.

```xml theme={null}
<disco_doc>
  <p>Welcome! This week we cover the basics.</p>
  <p align="center">See you in class.</p>
</disco_doc>
```

## Document structure

Every document is one `disco_doc` element.

You may start the document with an XML declaration or a bare `<!DOCTYPE disco_doc>`; both are
ignored, and Disco never writes them. A DOCTYPE that declares entities or points at an external
DTD is rejected, as is any processing instruction. Comments are ignored.

Content is a sequence of **blocks**. Most blocks hold **rich text**: ordinary text plus inline
elements such as line breaks. Some blocks hold other blocks instead. No block holds both, so
`<p><p>x</p></p>` is an error, and an inline element never appears directly under the root or
inside a block that holds blocks. Every element belongs to one category, which fixes where it
may appear and what it may contain:

| Category | Where it may appear | What it may contain | Elements |
| - | - | - | - |
| **Text block** | Inside `disco_doc` or any element that holds blocks | Rich text | `p` |
| **Empty inline** | Inside rich text | Nothing | `br`, `tab` |

### `disco_doc`

The root of every document. Contains blocks.

No attributes.

```xml theme={null}
<disco_doc>
  <p>Welcome to the course.</p>
</disco_doc>
```

* An empty document is `<disco_doc><p/></disco_doc>`. `<disco_doc/>` means the same thing.

## Naming

* **Element names** are lowercase and case-sensitive. HTML names are used where HTML has the
  same concept and Disco-specific names where it does not.
* **Attribute names** are `snake_case`. Values of enumerated attributes are lowercase.
* **There are no aliases.** Each element and attribute has exactly one spelling. Familiar
  alternatives such as `paragraph` are rejected, and the error names the Disco XML spelling
  to use instead.
* **HTML styling attributes are never accepted.** `style`, `class`, `id`, and `data-*` are
  rejected on every element. Presentation comes only from the elements and attributes in this
  document.

## Whitespace

In this section, whitespace means spaces, tabs, and newlines. Other Unicode spaces, such as a
non-breaking space, are ordinary text.

**Between blocks**, whitespace is ignored. Indent your document however you like. Disco
indents two spaces per level when it produces XML.

**Inside a block**, text is preserved exactly as written, with two exceptions that let you
wrap long paragraphs across lines in your source:

1. A run of whitespace that contains a newline is collapsed to a single space.
2. Such a run is removed instead at the very start or very end of a block and on either side
   of a `<br/>`, so tags can sit on their own lines.

So this source:

```xml theme={null}
<p>
  A long paragraph can be
  wrapped across lines.
  <br/>
  Second line.
</p>
```

is the same content as `<p>A long paragraph can be wrapped across lines.<br/>Second line.</p>`,
which is how Disco writes it back.

Everything else is significant: a single space and a double space between two words are
different content. Disco writes each block's text on a single line.

A line break inside a block is `<br/>`; a newline in your source never becomes one. A tab
is `<tab/>`. A literal tab character is kept as a tab unless it sits in a whitespace run that
contains a newline, where it collapses with the run, so write `<tab/>` for a tab you mean as
content.

## Escaping

Only these need escaping:

| Where | Character | Write |
| - | - | - |
| In text | `<` | `&lt;` |
| In text | `&` | `&amp;` |
| In text | `]]>` | `]]&gt;` |
| In an attribute value | `"` | `&quot;` |
| In an attribute value | `<` | `&lt;` |
| In an attribute value | `&` | `&amp;` |

Everything else, including `>`, `/`, quotes in text, backticks, and braces, is written as is.
Numeric character references such as `&#9;` are accepted. The only named entities are XML's
own five, `&lt;`, `&gt;`, `&amp;`, `&quot;`, and `&apos;`, so `&nbsp;` is an error. A bare
`&`, a `<` that does not begin a tag, and control characters are errors too, each with a
message saying what to write instead.

If you are pasting text with many `<` or `&` characters, you may wrap it in a CDATA section:
everything between `<![CDATA[` and `]]>` is taken literally, so the text itself cannot contain
`]]>`. Disco accepts CDATA anywhere text is allowed and never writes it; content you read back
always uses `&lt;`, `&amp;`, and `]]&gt;`.

## Attributes

Every attribute in this document is one of three kinds:

| Kind | When writing | When reading |
| - | - | - |
| **Required** | Must be present. | Always present. |
| **Optional** | May be omitted; the stated default applies. Writing the default explicitly is always accepted. | Present only when the value differs from the default. |
| **Read-only** | Ignored if present. Disco fills it in. | Present when Disco has the value. |

Any attribute not listed for an element is an error.

## Text blocks

### `p`

A paragraph. Contains rich text.

| Attribute | Kind | Values | Default | Description |
| - | - | - | - | - |
| `align` | Optional | `left`, `center`, `right`, `justify` | `left` | Horizontal alignment of the text. |
| `indent` | Optional | whole number `0`–`20` | `0` | How far the paragraph is indented, in steps from the left margin. |

```xml theme={null}
<p align="center" indent="1">Read this before class.</p>
```

* An empty paragraph is `<p/>`.

## Line breaks and tabs

Empty inline elements that may appear anywhere in a block's text.

### `br`

A line break inside a block. Empty; Disco writes it as `<br/>`.

No attributes.

```xml theme={null}
<p>First line<br/>second line</p>
```

* A newline in your source is not a line break; see Whitespace.

### `tab`

A tab inside a block. Empty; Disco writes it as `<tab/>`.

No attributes.

```xml theme={null}
<p>Name<tab/>Value</p>
```

* A literal tab character is also accepted; see Whitespace.

## Validation

Writing is all-or-nothing. If any part of a document is invalid, nothing is saved and you
receive a list of at most 100 errors. Each error carries:

* `code`, one of the codes below, and `message`, a sentence saying what to write instead.
* `line` and `column` in your XML, when the error has a position.
* `path`, the element at fault such as `disco_doc/p[2]`.
* `attribute`, the attribute at fault, when there is one.
* `allowed`, the accepted values, when they are a fixed set.
* `suggestion`, the name you probably meant, when there is a likely one.

| Code | Meaning |
| - | - |
| `malformed_xml` | The document is not well-formed XML: an unclosed tag, an unknown entity, an unescaped `&` or `<`, text outside the root, a DOCTYPE with declarations, a processing instruction. |
| `unknown_element` | An element name that is not in this specification. |
| `unknown_attribute` | An attribute that the element does not accept. |
| `missing_attribute` | A required attribute is absent. |
| `invalid_attribute_value` | A value outside its allowed set or a number out of range. |
| `id_mismatch` | The id attribute does not match the element's `kind`. |
| `invalid_containment` | An element appears somewhere it is not allowed, contains something it may not, or is nested more than 25 levels deep. |
| `unresolved_reference` | An id that does not exist or belongs to another community. |
| `forbidden_in_context` | The element is valid but not supported where you are writing. |
| `permission_denied` | The write implies an action you are not permitted to take. |

## What you get back

When you read content, Disco produces XML in a fixed, predictable form:

* No XML declaration, DOCTYPE, or comments. The root is `disco_doc`.
* One block per line, indented two spaces per level. Text within a block on a single line.
* Attributes in the order listed in this document. Optional attributes omitted when they hold
  the default.
* Empty elements written self-closing: `<p/>`, `<br/>`.

You are not required to write in this form. Disco accepts any layout and attribute order
allowed by this document.

## Full example

A complete document in the form Disco writes it back.

```xml theme={null}
<disco_doc>
  <p align="center">Week 1: Foundations</p>
  <p>Welcome! This week we cover the basics of Disco XML: paragraphs, line breaks, and tabs.</p>
  <p indent="1">Indented one level, for a note that belongs with the paragraph above.</p>
  <p>Office hours<tab/>Tuesday, 2 pm<br/>Reading due<tab/>Friday</p>
  <p>Remember that 1 &lt; 2, and that Q&amp;A follows every session.</p>
  <p/>
  <p align="right">See you in class.</p>
</disco_doc>
```


## Related topics

- [SCORM](/scorm.md)
- [Disco AI](/ai.md)
- [Welcome to Disco](/welcome-to-disco.md)
