Describe blocks and their units
When the body of a section must contain certain paragraphs, lists, tables, or code, declare them under the blocks of the section. At the end, a body with a missing block, an extra block, or a unit of the wrong shape fails. The error names the line of the document where the problem is.
Before you begin
- A schema with sections. Describe sections and headings shows how to write one.
- Templates and patterns, as in Write the shape of a line.
Steps
-
List the blocks of the body in order, each with a
kind:prose,bullet-list,numbered-list,table,code,blockquote,image,html,thematic-break,references(link reference definitions), orgroup. tramlines reads the body as CommonMark blocks. Consecutive paragraphs form oneproseblock, and a paragraph of only images forms animageblock. tramlines skips HTML comments, footnote definitions, and generated blocks when it matches blocks.If you leave out
blocks, the body must be one prose block. Writeblocks: []for an empty body. -
Bound the units of each block. A unit is a paragraph of a prose block or a blockquote, an item of a list, a row of a table, or one repetition of a group. By default, a block needs at least one unit and has no upper limit. Set
minormaxto change these bounds. -
Set
optional: trueon a block that a body can leave out. Setrepeat: trueon a block that can occur more than once in a row. To accept the blocks of a body in any order, setblocks-ordered: falseon the section. -
Give the units a shape:
eachapplies one rule to every unit.sequenceapplies one rule to each position, in order, and so sets the number of units. Setoptionalorrepeaton an entry to let its position be absent or recur.textsets the whole text of a unit.{ from: description }requires the unit to equal the frontmatter value, asheading: { from: title }does for a heading.
A block takes only one of
each,sequence, orrules. Select and count units and lines describesrules. -
Describe what an item contains besides its own text. tramlines matches the text of an item without its nested blocks. To allow a nested list or code block, describe it under
blockson the rule of the item. Withoutblockson the rule, an item can contain only its text. -
Add the keys of each kind:
- A
codeblock needslang: a tag, a list of tags, orany. A fence without a tag always fails.fencesets the minimum number of backticks. The default is three. - A
tableneedscolumns, the cells of its header row in order.columns-open: trueaccepts more columns after them. A unit of a table is a row, and a template matches the row as its cells separated by|. - An
imageblock takessource-pattern, a regular expression that each image path must match. - Any block takes
inline-html: true. Without it, HTML other than a comment fails.
A key that doesn’t apply to the kind of its block is an error in the schema. This schema describes the notes of a meeting:
- A
meeting.yaml
line-limit: 80
frontmatter:
title:
example: Weekly sync
description:
example: The team agreed the release date.
types:
date: '\d{4}-\d{2}-\d{2}'
sections:
- heading: { from: title }
level: 1
blocks:
- kind: prose
max: 1
each: { text: { from: description } }
- kind: bullet-list
sequence:
- { template: "Date: ${:date}", example: "Date: 2026-09-30" }
- { template: "Chair: ${}", example: "Chair: Ana" }
- heading: Attendees
level: 2
blocks:
- kind: table
columns: [Name, Role]
each: { template: "${} | ${}", example: "Ana | Chair" }
- heading: Decisions
level: 2
blocks:
- kind: numbered-list
max: 5
each:
template: "${}."
example: "Ship version 2 on Friday."
blocks:
- kind: blockquote
optional: true
example: Ana asked for one more day of testing.
- kind: code
lang: [text, console]
optional: true
example: npm run release
- heading: Photos
level: 2
optional: true
blocks:
- kind: image
source-pattern: '^images/.+\.png$'
example: ""
- Check a document:
sync.md
---
title: Weekly sync
description: The team agreed the release date.
---
# Weekly sync
The team agreed the release date.
- Date: 2026-09-30
- Chair: Ana
## Attendees
| Name | Role |
| ---- | ----- |
| Ana | Chair |
| Ben | Notes |
## Decisions
1. Ship version 2 on Friday.
> Ana asked for one more day of testing.
2. Move the retro to Monday.
```text
npm run release
```
## Photos

$ tramlines validate --schema meeting.yaml sync.md
0 errors, 0 warnings
- Read the errors for blocks. In this document, the second line of the sequence is missing, a paragraph stands before the table, the table has a different header row, a decision contains HTML, the code block has a tag that the schema doesn’t list, and the photo is in the wrong folder:
sync.md
---
title: Weekly sync
description: The team agreed the release date.
---
# Weekly sync
The team agreed the release date.
- Date: 2026-09-30
## Attendees
Everyone came.
| Name | Team |
| ---- | ---- |
| Ana | Web |
## Decisions
1. Move the retro to <b>Monday</b>.
```sh
npm run release
```
## Photos

$ tramlines validate --schema meeting.yaml sync.md
sync.md:10: error[TLMD_147]: position 2 of the sequence is missing (schema meeting, section "<title>" block 2)
sync.md:14: error[TLMD_39]: unexpected prose block (schema meeting, section "Attendees")
sync.md:16: error[TLMD_44]: the table header is `Name | Team`; expected `Name | Role` (schema meeting, section "Attendees" block 1)
sync.md:22: error[TLMD_45]: inline HTML is not accepted here (schema meeting, section "Decisions" block 1)
sync.md:24: error[TLMD_43]: the code block is tagged `sh`; expected `text` or `console` (schema meeting, section "Decisions" block 2)
sync.md:30: error[TLMD_163]: the image `board.png` does not match `^images/.+\.png$`; a target of this kind reads  (schema meeting, section "Photos" block 1)
6 errors, 0 warnings
[exit 1]
A block that the section doesn’t declare causes one error, and tramlines still checks the next block against its rule. Here the paragraph under Attendees fails, and tramlines validates the table as usual. Each error names a block by its position in the section. The name "<title>" stands for a heading taken from the frontmatter.
- To repeat a run of blocks as one unit, wrap them in a
groupwith its ownblocks. A group always repeats, sominandmaxbound the number of repetitions. Here each repetition is a paragraph with a question, followed by a paragraph with its answer:
faq.yaml
line-limit: 60
sections:
- heading: FAQ
level: 1
blocks:
- kind: group
min: 2
blocks:
- kind: prose
max: 1
each: { template: "Q: ${}?", example: "Q: Can I bring a guest?" }
- kind: prose
max: 1
example: Yes, tell the host a day before.
faq.md
# FAQ
Q: Can I bring a guest?
Yes, tell the host a day before.
$ tramlines validate --schema faq.yaml faq.md
faq.md:3: error[TLMD_146]: the group block needs at least 2 repetitions; it has 1 (schema faq, section "FAQ" block 1)
1 error, 0 warnings
[exit 1]
- Add a second question and answer:
faq.md
# FAQ
Q: Can I bring a guest?
Yes, tell the host a day before.
Q: Is there parking?
No, take the bus.
$ tramlines validate --schema faq.yaml faq.md
0 errors, 0 warnings
The two questions and two answers form four paragraphs in a row. tramlines divides them among the rules. When two rules of the same kind follow each other, the first takes as many units as its max allows and its shape accepts. The next rule takes the rest. How blocks are matched to a schema explains how tramlines matches each block to a rule.
Troubleshooting
TLMD_39: the body contains a block that the section doesn’t declare. See Errors: blocks.TLMD_152: the body lacks a block that the section declares. See TLMD_152.TLMD_69: the body contains a block of another kind where the schema expects a declared block. See TLMD_69.TLMD_146,TLMD_160: a block has fewer units than itsmin, or more than itsmax. See TLMD_146 and TLMD_160.TLMD_147,TLMD_161: a block has fewer units than itssequencehas entries, or more. See TLMD_147 and TLMD_161.TLMD_162: an item contains a nested list or code block that its rule doesn’t declare underblocks. See TLMD_162.TLMD_42: a block declares a key that doesn’t apply to its kind. See TLMD_42.TLMD_153: a block lacks a key that its kind requires. See TLMD_153.TLMD_47: a block declaresrulesbesideeachorsequence. Keep one. See TLMD_47.