Spec-driven document rendering engine — transform Markdown into publication-ready PDF, HTML, DOCX, and more.
The official documentation site is published at https://egohygiene.github.io/renderflow/ and includes versioned guides, architecture references, generated format support tables, examples, troubleshooting, and FAQ content.
Renderflow is a config-driven rendering engine that transforms documents into polished output — no complex shell scripts, no Pandoc flags to memorize.
Define your output spec in YAML. Point it at your source. Run one command.
At its core, Renderflow models every transformation as a directed acyclic graph (DAG). The graph engine finds the optimal path between your source format and each target, shares intermediate steps across outputs, and executes independent conversions in parallel using Rayon.
- 📄 Multi-format output — Render to PDF, HTML, DOCX, and more from a single config file
- 🗂️ YAML-driven spec — Declarative, repeatable, version-controllable builds
- 🔀 Graph engine — DAG-based transform planner with parallel execution and shared intermediate steps
- 🎯 Optimization modes — Choose Speed, Quality, Balanced, or Pareto-optimal path selection
- 🔄 Transform pipeline — Pluggable in-memory content transforms (built-in and custom)
- 🤖 AI transforms — Ollama and OpenAI-compatible LLM integration with local caching
- 🖼️ Image conversion — FFmpeg-backed format conversion across 80+ image formats (JPEG, PNG, WebP, AVIF, HEIC, EXR, and more)
- 🎵 Audio conversion — FFmpeg-backed format conversion across 40+ audio formats (WAV, FLAC, MP3, AAC, Opus, and more)
- 🧩 Custom templates — Per-output Jinja2-compatible templates via Tera
- 🔌 Plugin system — Register external transform executors at runtime without modifying core
- 🖼️ Asset management — Automatically resolves and validates image paths
- 🔍 Dry-run mode — Preview what will be built without writing any files
- 👁️ Watch mode — Automatically rebuild on file changes with configurable debounce
- ⚡ Incremental builds — Content-hash-based caching skips unchanged outputs
- 🦀 Built with Rust — Fast, safe, and reliable
1. Create your Markdown document (input.md):
# My Document

Welcome to my publication.2. Create a config file (renderflow.yaml):
input: "input.md"
output_dir: "dist"
outputs:
- type: pdf
- type: html3. Render:
renderflow buildOutput files appear in dist/:
dist/
├── input.pdf
└── input.html
brew trust egohygiene/renderflow
brew tap egohygiene/renderflow https://github.com/egohygiene/renderflow
brew install renderflowNote: The
brew truststep is required before tapping because this is a third-party tap. Without it, Homebrew will refuse to load the formula with an "untrusted tap" error.
Pandoc is installed automatically as a dependency.
scoop bucket add egohygiene https://github.com/egohygiene/renderflow
scoop install renderflowchoco install renderflowsnap install renderflow --classiccurl -fsSL https://raw.githubusercontent.com/egohygiene/renderflow/main/scripts/install.sh | shYou can pin a release version with RENDERFLOW_VERSION and override install location with RENDERFLOW_INSTALL_DIR.
Stable release:
yay -S renderflowLatest git build:
yay -S renderflow-gitDownload the .deb for your architecture from the Releases page and install:
sudo dpkg -i renderflow_*.debDownload the .rpm for your architecture from the Releases page and install:
sudo rpm -i renderflow-*.rpmPre-built binaries are available for Linux (x86_64, aarch64, ARMv7, i686), macOS (Intel, Apple Silicon), and Windows (x86_64) on the Releases page.
| Platform | Binary |
|---|---|
| Linux x86_64 (musl) | renderflow-x86_64-unknown-linux-musl |
| Linux x86_64 (glibc) | renderflow-x86_64-unknown-linux-gnu |
| Linux aarch64 (musl) | renderflow-aarch64-unknown-linux-musl |
| Linux aarch64 (glibc) | renderflow-aarch64-unknown-linux-gnu |
| Linux ARMv7 | renderflow-armv7-unknown-linux-musleabihf |
| Linux i686 | renderflow-i686-unknown-linux-musl |
| macOS Intel | renderflow-x86_64-apple-darwin |
| macOS Apple Silicon | renderflow-aarch64-apple-darwin |
| Windows x86_64 | renderflow-x86_64-pc-windows-msvc.exe |
cargo install --path .renderflow --version
renderflow version
renderflow env
renderflow doctor# Render using the default renderflow.yaml config
renderflow build
# Render using a custom config file
renderflow build --config my-project.yaml
# Shorthand: pass the config file directly
renderflow my-project.yaml
# Preview what would be built, without writing any files
renderflow build --dry-run
# Enable verbose or debug logging
renderflow build --verbose
renderflow build --debugNote: The argument to
renderflowis always a YAML config file. The Markdown source is specified inside the config via theinputkey (e.g.input: "input.md").
Renderflow also ships as a reusable Rust crate for embedding in your own tools.
cargo add renderflowRunnable SDK examples are included in this repository:
cargo run -p renderflow --example library_basic
cargo run -p renderflow --example plugin_registration
cargo run -p renderflow --example embedded-basic
cargo run -p renderflow --example embedded-multi-targetProduce PDF, HTML, and DOCX from one config:
input: "report.md"
output_dir: "dist"
outputs:
- type: pdf
- type: html
template: "default"
- type: docxInject dynamic values that are replaced at build time:
input: "report.md"
output_dir: "dist"
variables:
title: "Q4 Report"
author: "Jane Smith"
outputs:
- type: htmlreport.md:
# {{title}}
*Written by {{author}}*Point an output at a custom Tera template stored in templates/:
input: "report.md"
output_dir: "dist"
outputs:
- type: html
template: "newsletter"Renderflow will render using templates/newsletter.html (Jinja2-compatible Tera syntax).
By default, Renderflow infers the input format from the file extension. Use input_format to override this explicitly — useful when the file extension doesn't match the content, or when working with formats such as RST, HTML, or EPUB:
input: "document.rst"
input_format: rst # explicitly tell Pandoc to read this as reStructuredText
output_dir: "dist"
outputs:
- type: html
- type: pdfSee Supported Input Formats for the full list of accepted values.
Watch mode monitors your source files and automatically rebuilds whenever a change is detected.
# Watch using the default renderflow.yaml config
renderflow watch
# Watch using a custom config file
renderflow watch --config my-project.yaml
# Override the debounce delay (default: 500 ms)
renderflow watch --config my-project.yaml --debounce 300How it works:
- An initial build runs immediately when watch mode starts.
- Renderflow watches the config file, the input document, and the
templates/directory for changes. - After a file change is detected, Renderflow waits for the debounce delay (default: 500 ms) before triggering a rebuild — so rapid successive saves don't cause redundant builds.
- Build errors are logged but do not stop the watcher; the next save will trigger another attempt.
File watching scope:
| Watched path | Mode | Notes |
|---|---|---|
| Config file | Non-recursive | e.g. renderflow.yaml |
| Input document | Non-recursive | Path from the input key |
templates/ dir |
Recursive | Watched when the directory exists |
Press Ctrl+C to stop watch mode.
Renderflow is entirely driven by a YAML spec file (default: renderflow.yaml):
input: "input.md" # Path to your source document
input_format: markdown # Optional: override auto-detected format
output_dir: "dist" # Output directory (default: dist)
variables: # Optional: key/value pairs for substitution
title: "My Document"
author: "Jane Smith"
outputs:
- type: pdf # Render to PDF (requires Pandoc + Tectonic)
- type: html # Render to HTML
template: "default" # Optional: use a custom Tera template
- type: docx # Render to Word document| Key | Required | Default | Description |
|---|---|---|---|
input |
✅ Yes | — | Path to the source document (Markdown, HTML, RST, etc.) |
input_format |
❌ No | auto-detect | Override the input format; auto-detected from file extension when omitted |
output_dir |
❌ No | dist |
Directory where output files are written |
outputs |
✅ Yes | — | List of one or more output targets (must contain at least one entry) |
outputs[].type |
✅ Yes | — | Output format: html, pdf, docx, or any supported audio/image format |
outputs[].template |
❌ No | — | Name of a Tera template in the templates/ directory to use for this output |
variables |
❌ No | {} |
Map of string key/value pairs injected into the document via {{key}} placeholders |
The input_format key (or the file extension of input) controls how Pandoc reads the source document.
| Value | File Extensions | Notes |
|---|---|---|
markdown |
.md, .markdown |
Default when extension is unknown |
html |
.html, .htm |
|
rst |
.rst |
reStructuredText |
docx |
.docx |
|
epub |
.epub |
|
latex |
.tex |
When input_format is omitted, Renderflow auto-detects the format from the file extension and falls back to markdown when the extension is unrecognised.
Document formats:
| Type | Description | Requirements |
|---|---|---|
html |
Renders to HTML | Pandoc |
pdf |
Renders to PDF via LaTeX | Pandoc + Tectonic |
docx |
Renders to Word document | Pandoc |
Not every input → output combination is supported. For example, epub and latex inputs cannot currently be converted to docx. Renderflow reports a clear error when an unsupported combination is specified.
Image formats (via FFmpeg):
Common formats: jpeg, png, webp, avif, gif, bmp, tiff
Professional/HDR: exr, hdr, dpx, cin
Audio formats (via FFmpeg):
Lossless: wav, flac, aiff, alac
Lossy: mp3, aac, ogg, opus, wma
Multichannel: ac3, dts, truehd
Templates live in a templates/ directory and use Tera syntax (Jinja2-compatible). Specify a template per output with the template key. A default HTML template is included out of the box.
Define a variables map in your config to inject dynamic values into your document:
variables:
title: "Q4 Report"
author: "Jane Smith"
version: "1.0"Reference them in your Markdown using {{key}} syntax:
# {{title}}
*Written by {{author}}*
Version: {{version}}Placeholders for undefined keys are left unchanged, and a warning is emitted so you can spot typos.
Before any output is written, Renderflow applies a series of in-memory text transforms to the source document. Transforms run in order after the file is read and before Pandoc processes it.
Replaces emoji characters with the literal text [emoji].
Why: PDF and some LaTeX backends cannot render Unicode emoji directly. This transform ensures the pipeline doesn't crash on emoji-heavy content.
Format-aware behaviour: When rendering to HTML, emoji are preserved unchanged because browsers render them natively. For PDF and DOCX outputs, emoji are always replaced with [emoji].
Example:
| Input | PDF / DOCX output | HTML output |
|---|---|---|
Hello 😀 World |
Hello [emoji] World |
Hello 😀 World |
🎉 Party time! 🎉 |
[emoji] Party time! [emoji] |
🎉 Party time! 🎉 |
Limitation: The replacement is a plain-text placeholder. Full SVG/image-based emoji embedding is planned for a future release.
Replaces {{key}} placeholders in the source document with values defined in the variables map of your config.
When it runs: Before Pandoc, so substituted values are part of the rendered content.
Behaviour:
- Keys are matched exactly (whitespace around the key name is trimmed, so
{{ title }}and{{title}}are equivalent). - If a placeholder references a key that is not in
variables, the placeholder is left unchanged and a warning is emitted. - Unclosed placeholders (e.g.
{{unclosed) are also left unchanged. - Code block protection: Placeholders inside fenced code blocks (
``` ... ```) and inline code spans (`...`) are not substituted, so example code in your document is never corrupted.
Normalises the language tags on fenced code blocks (```) to lowercase with surrounding whitespace stripped.
When it runs: Before Pandoc, ensuring consistent language identifiers are passed to the syntax highlighting engine.
Example:
| Input fence | Normalised fence |
|---|---|
```Rust |
```rust |
``` Python |
```python |
```JavaScript |
```javascript |
Applies an LLM prompt to document content before rendering.
Backends:
| Backend | Endpoint | Notes |
|---|---|---|
ollama |
http://localhost:11434 (default) |
Local model via Ollama |
openai |
OpenAI-compatible API | Prefer api_key_env; api_key remains supported for compatibility |
Caching: Responses are cached locally using a SHA-256 hash of the prompt + input content. Unchanged content is never re-sent to the model.
Secure API keys: Prefer api_key_env so secrets stay out of YAML:
transforms:
- name: ai-transform
ai: openai
model: gpt-4o-mini
api_key_env: OPENAI_API_KEY
from: markdown
to: html
cost: 1.0
quality: 0.9Pipes document content through an external command.
Pipe-based (stdin → stdout):
transforms:
- name: my-filter
command: pandoc-filter
args: []File-based (uses {input} / {output} placeholders):
transforms:
- name: my-filter
command: my-tool
args: ["--in", "{input}", "--out", "{output}"]Register custom transform executors at runtime without modifying the core engine. Implement the PluginExecutor trait in your own crate and register it with the PluginRegistry:
use renderflow::transforms::plugin::PluginExecutor;
struct ReversePlugin;
impl PluginExecutor for ReversePlugin {
fn name(&self) -> &str { "reverse" }
fn execute(&self, input: String) -> anyhow::Result<String> {
Ok(input.chars().rev().collect())
}
}See it in action: The
examples/transforms/directory contains a complete working example that exercises all three built-in transforms.
Renderflow provides two transform failure modes that control what happens when a transform encounters an error during document processing.
The pipeline aborts immediately on the first transform error and surfaces the error to the caller. This is the default when running renderflow build.
# renderflow.yaml — default behaviour: a failed transform stops the build
outputs:
- type: htmlWhen to use: In CI pipelines and production builds where a broken transform should be a hard failure.
The failing transform is skipped, an error is logged, and the pipeline continues with the unmodified input passed to the next transform.
Renderflow uses ContinueOnError automatically in watch mode (renderflow watch) so that a single transform failure does not halt the watcher — the next file-save will trigger another attempt.
# renderflow.yaml — watch mode implicitly uses ContinueOnError
outputs:
- type: htmlWhen to use: Watch mode and resilient rebuild pipelines where best-effort output is preferable to a complete failure.
AI transforms and command transforms are always optional in the sense that they only run when explicitly configured. If the external backend (Ollama, OpenAI, external tool) is unavailable the transform fails, and the active failure mode determines whether the build aborts or continues.
# Omitting the ai/command key means the transform is simply not registered
outputs:
- type: html
# No transforms key → no AI or command transforms are appliedWhen an AI transform (AiTransform) fails — for example, because the Ollama server is not running or the API key is invalid — the error is propagated according to the active failure mode:
- FailFast (default): build aborts with a clear error message identifying the failing transform.
- ContinueOnError (watch mode): the AI transform is skipped, the original document content passes through unchanged, and the build continues.
Cached responses are reused across builds: if the same prompt + content hash was seen in a previous run, the cached result is returned without contacting the model, so AI failures only occur when the cache is cold or invalidated.
Renderflow's transform planner models every conversion as a directed acyclic graph (DAG) backed by petgraph.
Each format is a node; each supported conversion is a weighted directed edge. When you specify multiple output targets, the planner:
- Finds the shortest path from your source format to each target.
- Merges paths into a single DAG, reusing shared intermediate steps (e.g.
Markdown → HTMLis computed once when producing both PDF and DOCX). - Groups independent edges into execution waves and runs each wave in parallel using Rayon.
Control how the planner selects transformation paths:
| Mode | Strategy |
|---|---|
speed |
Minimise total transformation cost |
quality |
Maximise total output quality |
balanced |
Weighted combination of cost and quality (default) |
pareto |
Return the full Pareto-optimal frontier of non-dominated paths |
renderflow build --optimization qualityRenderflow tracks a SHA-256 content hash for every DAG node. On subsequent builds, nodes whose input hash is unchanged are skipped entirely — only the affected subgraph is re-executed.
Renderflow processes documents through a layered pipeline:
Input Document
│
▼
┌─────────────────────────────────────────┐
│ Transform Phase │
│ 1. EmojiTransform │ Replace emoji (format-aware)
│ 2. VariableSubstitutionTransform │ Substitute {{key}} placeholders
│ 3. SyntaxHighlightTransform │ Normalise code fence language tags
│ 4. AiTransform (optional) │ LLM-powered content processing
│ 5. CommandTransform (optional) │ External command filters
│ 6. Plugin transforms (optional) │ Runtime-registered extensions
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Graph Engine │
│ • DAG construction (petgraph) │ Model formats as nodes, conversions as edges
│ • Path optimisation │ Speed / Quality / Balanced / Pareto
│ • Shared intermediate deduplication │ Compute Markdown→HTML once for PDF+DOCX
│ • Parallel wave execution (Rayon) │ Independent edges run concurrently
│ • Incremental hash cache │ Skip unchanged nodes
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Step Phase │
│ • Strategy dispatch │ Select renderer per output type
│ • I/O (Pandoc, Tectonic, FFmpeg) │ External tool execution
│ • Artifact collection │ Gather outputs for the release step
└─────────────────────────────────────────┘
│
▼
Output Files (PDF / HTML / DOCX / images / audio / …)
Key design patterns:
- Pipeline — Ordered, composable steps with clean error propagation
- Strategy — Each output format is an independent, swappable rendering strategy
- Transform — Pure in-memory text transforms applied before any I/O
- DAG — Transform paths are modelled as graphs for optimal multi-target execution
Renderflow is organized as a Rust workspace:
crates/renderflow-core(renderflow) — reusable SDK and enginecrates/renderflow-cli— thin command-line adapter that calls the core APIcrates/renderflow-plugin-sdk— plugin-facing type boundary
The CLI does not reimplement planning or execution logic; it delegates to the core crate.
- The
renderflowpublic API follows semantic versioning. - New APIs may be added in minor releases; breaking changes are reserved for major releases.
- Deprecated APIs remain available for at least one minor release before removal.
Releases are fully automated. Run the Bump Version workflow from GitHub Actions:
- Navigate to Actions → Bump Version → Run workflow.
- Select the bump level (
patch/minor/major) or enter an explicit version. - The workflow bumps
Cargo.toml, updates the workspace package version inCargo.lock, updates package manifest versions, creates an annotated tag, pushes tomain, and dispatches the release pipeline.
The Release workflow then:
- Generates
CHANGELOG.mdand release notes withgit-cliff. - Cross-compiles release binaries for all 10 supported targets.
- Builds
.deb,.rpm,.snap, and.nupkgpackages. - Uploads all artifacts to the GitHub Release.
- Updates Homebrew, Scoop, and AUR package manifests with retry/rebase safeguards.
- Verifies required binaries, checksums, package artifacts, and source tarball availability.
- Built-in stylesheet themes
- SVG / emoji embedding in PDFs
- More example configs and templates
- Automated release workflow for pre-built binaries
- Graph engine with DAG-based transform planner
- AI transform integration (Ollama / OpenAI)
- Audio and image format conversion via FFmpeg
- Plugin system for custom transforms
MIT © Ego Hygiene