Sashmark / Blog

Read YAML front matter as a table, not raw text

Bobby Huang ·

To read YAML front matter fast, turn it into a table: one row per field, nested fields flattened to a dotted name like review.due, and, in the Python table, lists shown as one comma list. The Go-based yq, version 4, prints each field as a key = value line and can read one field on its own. A short Python script with PyYAML prints a Markdown table you can paste into any file. Both tools stop with an error if the header is not valid YAML.

Key takeaways

  • A table puts one field on each row, so status, owner and dates are easy to find at a glance.
  • Flatten the header: a nested field becomes a dotted name such as review.due.
  • The Go-based yq, version 4, prints front matter as a key = value list.
  • A short Python script with PyYAML prints the header as a Markdown table and escapes pipes so the table still renders.
  • Sashmark is in development and not released; its Frontmatter Table is planned for v1.

Agents like to start their Markdown files with a YAML header: a status, an owner, a couple of dates, some tags, and often a nested block for review. That header is the fastest way to know what a file is and whether it needs you. Raw YAML hides it well, though. Indentation decides what belongs to what, lists sit on one line or many, and the field you want can be anywhere in the block.

This page shows two quick ways to read YAML front matter as a table instead. The first uses yq to print each field as a key = value line. The second is a short Python script that prints a real Markdown table. Both run in a terminal.

If you need the basics first, start with what front matter is. This post assumes you already have files with a header and want to scan them faster.

Why a table reads faster than raw YAML

A table gives each field its own row. The field name sits in the left column and the value sits in the right, so your eye runs straight down one edge to find status or owner.

Two small changes make the table easier still:

  • Nested fields get a dotted name. A due field inside a review block becomes review.due. You see where it lives without counting spaces.
  • Lists become one comma list. In the Python table below, tags written as [ui, settings] show as ui, settings in one cell.

That's the whole idea. The rules for how front matter is written, and which tools read it, are in YAML front matter in Markdown.

The sample file

Both recipes below use this file, saved as plan.md:

---
title: Settings page dark theme
status: in-progress
owner: Jane Doe
date: 2026-10-19
tags: [ui, settings]
review:
  by: Doe Holdings
  due: 2026-10-24
---

# Settings page dark theme

Body text.

Recipe 1: read YAML front matter with yq

These commands are for the Go-based yq, version 4. Another tool is also called yq (a Python wrapper around jq), and it takes different flags. If a command below fails on its flags, check which yq you have.

  1. Print only the header. This shows the YAML block (with its leading ---) and nothing from the body:

    yq --front-matter=extract '.' plan.md
    
  2. Print the header as a key = value list. The props output flattens everything for you:

    yq --front-matter=extract -o=props '.' plan.md
    

    Output:

    title = Settings page dark theme
    status = in-progress
    owner = Jane Doe
    date = 2026-10-19
    tags.0 = ui
    tags.1 = settings
    review.by = Doe Holdings
    review.due = 2026-10-24
    

    Nested fields already have dotted names. Lists show one item per line, numbered from 0.

  3. Read one field. Give yq the path to it:

    yq --front-matter=extract '.status' plan.md
    

    This prints in-progress. For a nested field, add the parent name:

    yq --front-matter=extract '.review.due' plan.md
    

    This prints 2026-10-24.

The single-field form is handy when you only care about one thing, like whether a file is still in progress.

Recipe 2: a short Python script that prints a table

If you want a real table, this script does it. It needs Python 3 and the PyYAML package. Save it as fm_table.py:

import sys, yaml

lines = open(sys.argv[1], encoding="utf-8").read().splitlines()
if not lines or lines[0] != "---":
    sys.exit("no front matter")
end = lines.index("---", 1)
data = yaml.safe_load("\n".join(lines[1:end]))

def rows(d, prefix=""):
    for key, value in d.items():
        name = f"{prefix}{key}"
        if isinstance(value, dict):
            yield from rows(value, name + ".")
        elif isinstance(value, list):
            yield name, ", ".join(str(v) for v in value)
        else:
            yield name, str(value)

def cell(text):
    return text.strip().replace("\n", " ").replace("|", "\\|")

print("| Field | Value |")
print("| --- | --- |")
for name, value in rows(data):
    print(f"| {cell(name)} | {cell(value)} |")

Run it on the sample file:

python3 fm_table.py plan.md

Output:

| Field | Value |
| --- | --- |
| title | Settings page dark theme |
| status | in-progress |
| owner | Jane Doe |
| date | 2026-10-19 |
| tags | ui, settings |
| review.by | Doe Holdings |
| review.due | 2026-10-24 |

Paste that output into a Markdown file and it renders as a table. If Markdown tables are new to you, that post covers the syntax.

A few details worth knowing:

  • Cells stay on one line. A pipe character would normally split a table cell, and a line break would end the row. The script turns | into \| and joins a value that spans several lines into one line, so a title like A | B test prints as | title | A \| B test | and the table still renders. Quoting values like that one in the YAML itself is covered in YAML front matter quoting.
  • The header has to come first. The script only reads a header that starts on the first line with --- and ends at the next line that is exactly ---. A file without that first line gets the message "no front matter", and a header that is never closed stops the script with an error.
  • It expects key and value pairs. The header should be a set of fields, not a bare list or a single value.

When the header is broken

Sometimes the header itself is the problem. Take a title like title: Settings: dark theme, with an unquoted colon in the middle. Neither tool guesses at what was meant.

yq prints this and exits with code 1:

Error: bad file 'bad.md': yaml: mapping values are not allowed in this context

The Python script stops with:

yaml.scanner.ScannerError: mapping values are not allowed here

That's useful. A loud error is better than a table with a wrong value in it. For what each error means and how to fix it, see YAML front matter errors.

Habits that make the table pay off

A table only helps if you read it with a purpose. These habits keep it quick:

  1. Check status, owner and dates first. They tell you whether the file needs you today. Which fields to include in the first place is covered in front matter fields for status, date and owner.
  2. Look for missing rows. A field that isn't in the header simply has no row. If you expect an owner and there's no owner row, the agent left it out. That's easier to spot in a short table than in a block of YAML.
  3. Read the dotted rows. Nested fields are the easiest to miss in raw YAML, because they hide under a parent at a deeper indent. In the table, review.due sits in the same column as everything else.
  4. Compare files with the same rows. Run the script on two plans and the field names line up, so a different owner or a later due date stands out.

Where Sashmark fits

Sashmark is a Markdown reader and editor for people whose AI agents write Markdown, being built for Mac, Windows and Linux. It is in development and has not been released, and the app itself has no AI features. A good reader should show the YAML header as a small key and value table above the document, with the raw YAML one step away for when you need to edit it. Sashmark is being built for this: its Frontmatter Table is planned for v1, along with the other planned features. The roadmap shows where it stands.

Sashmark is in development. See what it is at sashmark.com.

Questions

Which yq do these commands need?
The Go-based yq, version 4. Another tool with the same name is a Python wrapper around jq, and it takes different flags, so these commands will not work with it. Check which one you have before you copy the commands.
How do nested fields show up in the table?
They are flattened to a dotted name. A review block with by and due inside it becomes two rows, review.by and review.due. That makes nested fields as easy to scan as top-level ones.
What happens if the YAML header is broken?
Neither tool guesses. yq prints an error and exits with code 1, and the Python script stops with a YAML error. Fix the header first, then run the tool again.

What Sashmark plans for v1: Features · Roadmap

View as Markdown