Skip to content

Content Flows ​

Flows are Press's way of separating content from layout. A flow is a named container of content that can be painted into one or more frames across one or more pages. This separation is what makes Press templates truly reusable.

Defining Flows ​

Flows live inside the <flows> top-level element. Each flow has a tag name that becomes its identifier:

xml
<flows>
  <body>
    <h1>Annual Report</h1>
    <p>This year we achieved significant milestones...</p>
  </body>

  <sidebar>
    <h3>Key Figures</h3>
    <p>Revenue: $2.4M</p>
    <p>Growth: 18%</p>
  </sidebar>
</flows>

Referencing Flows ​

Inside a page definition, use <flow name="..." /> to place content from a flow:

xml
<document format="A4" page-margin="2cm">
  <page>
    <flow name="body" />
  </page>
</document>

Press fills the frame containing the flow reference with as much content as will fit.

Multi-Column Layouts ​

Reference the same flow multiple times to create columns. Press fills the first frame, then continues into the second:

xml
<page>
  <frame direction="row" width="100%" height="100%">
    <frame width="48%">
      <flow name="body" />
    </frame>
    <frame width="4%" />
    <frame width="48%">
      <flow name="body" />
    </frame>
  </frame>
</page>

The body content flows from the left column into the right column automatically.

Flowing Across Pages ​

To continue a flow across multiple pages, use <repeat flow="...">:

xml
<document format="A4" page-margin="2cm">
  <repeat flow="body">
    <page>
      <flow name="body" />
    </page>
  </repeat>
</document>

Press keeps generating pages and filling them with body content until the flow is fully consumed.

Alternating Page Layouts ​

Place multiple pages inside a repeat for alternating designs:

xml
<repeat flow="body">
  <page>
    <frame margin-bottom="0.5cm" font-color="grey" text-align="right">
      {{ data.author }}
    </frame>
    <flow name="body" />
  </page>
  <page>
    <frame margin-bottom="0.5cm" font-color="grey">
      {{ data.title }}
    </frame>
    <flow name="body" />
  </page>
</repeat>

This alternates between a right-aligned author header and a left-aligned title header, similar to how printed books alternate left and right pages.

Markdown Flows ​

Set type="markdown" on a flow to write content in markdown instead of Press XML:

xml
<flows>
  <body type="markdown">
    # Quarterly Review

    Our team delivered several key improvements this quarter:

    - Reduced processing time by **40%**
    - Launched three new product features
    - Expanded to two new markets

    ## Financial Summary

    Revenue grew steadily across all segments. The operations
    team maintained costs within budget while scaling output.
  </body>
</flows>

Press automatically strips leading indentation from markdown content.

Markdown from the payload ​

A markdown flow can take its content from an expression, so whole sections arrive as strings in the payload rather than as Press markup:

xml
<data type="json">
  { "summary": "## This month\n\nWe shipped **three** things:\n\n- one\n- two" }
</data>

<flows>
  <summary type="markdown">{{ data.summary }}</summary>
</flows>

The expression has to sit directly in the flow body. The same expression inside <repeat> or <show-if> renders its markdown as literal characters — - item keeps its hyphen, ## Heading its hashes — and the document still renders, with no warning. Use <markdown inline="true"> for each item instead, which covers bold, italics and links but not headings or lists.

What a payload string can carry ​

A payload string rendered through a markdown flow is read as Markdown, not as Press. A tag in the string is typeset as the characters it is written with — <h1> prints as <h1>, and the Markdown around it still renders, so a paragraph under a stray <div> keeps its bold. This is about copy that arrives as a string: markup you write into the flow yourself is applied as markup, which is Mixing Markdown and Press below.

Validation reports a tag in the string as a warning, so copy you did not author can be checked before you render it. Three further consequences worth knowing. A line break comes from a newline in the string, so <br /> prints rather than breaking one. A closed <!-- … --> is dropped, as a comment is written to be. And a long tag with no spaces in it — an <img src="data:image/png;base64,…">, the shape a model emits for a chart — is one unbreakable word: Papermill breaks it across lines for as long as the space the element has can hold it, rejects a longer one with message Element doesn't contain enough space for text, and rejects one over 10,000 characters with message Unbreakable run of 12345 characters exceeds the 10000-character limit: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAA…". Shorten it or add a space. (the count and the opening characters are the run's own); strip data URIs before sending.

An expression is different: {{ … }} resolves against the payload like any other expression, so {{ data.secret }} written into the copy renders the value.

Untrusted copy can also fail the render rather than leak: an expression naming a missing field, an unsupported operator, a wide table, a skipped heading level, or an image whose URL Papermill cannot reach — ![alt](url) and the ![alt][ref] form both fetch. Each names a position in the template rather than in the payload string.

Mixing Markdown and Press ​

You can embed Press elements directly inside a markdown flow when you need more control:

xml
<flows>
  <body type="markdown">
    # Project Status

    The project is on track for delivery in Q2.

    <frame direction="row" space-before-desired="12pt" space-after-desired="12pt">
      <frame width="fill" background-color="#f0fdf4" padding="12pt" border-radius="4pt">
        <h3 font-color="#16a34a">Completed</h3>
        <p>12 tasks</p>
      </frame>
      <frame width="8pt" />
      <frame width="fill" background-color="#fef2f2" padding="12pt" border-radius="4pt">
        <h3 font-color="#dc2626">Remaining</h3>
        <p>4 tasks</p>
      </frame>
    </frame>

    ## Next Steps

    The team will focus on integration testing next sprint.
  </body>
</flows>

Flow Groups ​

Use <group> to organise a flow into named sections. Groups are essential for documents with repeating section structures:

xml
<flows>
  <group name="chapters">
    <introduction type="markdown">
      <data type="json">
        { "chapter-title": "Introduction", "chapter-number": 1 }
      </data>
      This report examines market conditions across three regions...
    </introduction>

    <methodology type="markdown">
      <data type="json">
        { "chapter-title": "Methodology", "chapter-number": 2 }
      </data>
      Data was collected from public records spanning 2020 to 2025...
    </methodology>

    <findings type="markdown">
      <data type="json">
        { "chapter-title": "Findings", "chapter-number": 3 }
      </data>
      The analysis revealed consistent growth in urban centres...
    </findings>
  </group>
</flows>

Then iterate over the groups in your document:

xml
<document format="A4" page-margin="2cm">
  <repeat group="chapters" item="chapter">
    <page>
      <h1>{{ chapter.data['chapter-number'] }}. {{ chapter.data['chapter-title'] }}</h1>
      <flow name="chapter" />
    </page>
  </repeat>
</document>

Flow Data ​

Each flow (or group member) can include its own <data> element. This data is accessible within the flow and on any page that renders it:

xml
<flows>
  <group name="properties">
    <unit-a>
      <data type="json">
        { "address": "14 Maple Street", "bedrooms": 3, "price": "£425,000" }
      </data>
      <p>A charming three-bedroom property with garden views...</p>
    </unit-a>

    <unit-b>
      <data type="json">
        { "address": "7 Oak Avenue", "bedrooms": 2, "price": "£310,000" }
      </data>
      <p>Modern two-bedroom flat in a converted Victorian terrace...</p>
    </unit-b>
  </group>
</flows>

On the page, access the flow's data through the item variable:

xml
<repeat group="properties" item="property">
  <page>
    <h1>{{ property.data.address }}</h1>
    <p>{{ property.data.bedrooms }} bedrooms - {{ property.data.price }}</p>
    <flow name="property" />
  </page>
</repeat>

Anonymous Flows ​

You can define a flow directly inside a page without giving it a name. This is useful for one-off content:

xml
<page>
  <flow>
    <h1>Disclaimer</h1>
    <p>This document is for informational purposes only...</p>
  </flow>
</page>

It's also possible to give anonymous flows a name and have the content repeat over multiple pages. This is especially useful for things like long, multi-page table of contents:

xml
<!-- table of contents -->
<page flow="toc" repeat="true" deferred="true">
    <!-- in anon flow so heading only appears on first page as flow is then empty -->
    <flow name="toc-heading">
        <h1 outline="meta" padding-bottom="0.7cm" font-color="primary">Table of Contents</h1>
    </flow>
    <frame>
        <!--
            another anonymous flow to allow table of contents
            to be more than one page long
          -->
        <flow name="toc">
            <repeat outline="heading" item="h1">
                <!-- render h1s -->
                {{ h1.index }}. {{ h1['text-content'] }} - Page <value pad="2">{{ h1['page-number'] }}</value>
            </repeat>
        </flow>
    </frame>
</page>