Skip to content

DOCX Authoring Guide

Prerequisite: Read the Template Authoring Guide (Jinja Essentials) first -- it covers the Jinja basics that apply to all templates, including DOCX.

VIBE supports both Markdown and Word templates. If you prefer to draft in Microsoft Word, this guide covers the practical differences that matter when authoring .docx templates.

When to Choose DOCX

DOCX templates are a good fit when:

  • Your starting point is an existing Word document with established styles or branding
  • You need Word-native features such as tracked numbering, tables, headers, footers, or bookmarks
  • The final deliverable is primarily a Word document rather than web preview text

Markdown templates are often easier to diff and refactor, but DOCX is the better authoring format when visual fidelity in Word is the priority.

Where Jinja Syntax Goes

Use the same Jinja syntax in DOCX that you would use in Markdown:

{{ client_name }}
{% if include_appendix %}
{% endif %}
{% for signer in signers %}
{% endfor %}

In practice, place tags where ordinary text would normally go:

  • In a normal paragraph
  • Inside a table cell
  • Inside a list item
  • Inside a heading

Keep each tag contiguous in Word. A tag like {{ client_name }} should be typed as one continuous piece of text, not partially bolded or manually split across differently formatted fragments.

docxtpl Structural Tags

VIBE renders .docx templates with docxtpl, so Word templates support a few extra tags beyond plain Jinja. These matter whenever the logic needs to control an entire Word structure rather than just inline text.

  • {%p ... %} controls a paragraph
  • {%tr ... %} controls a table row
  • {%tc ... %} controls a table cell/column region
  • {%r ... %} controls a single Word run
  • {{r value }} inserts a RichText value into the current run
  • {{p value }} inserts paragraph-level content such as a rich paragraph or subdocument

Use these tags when ordinary Jinja would otherwise need to cross paragraph, row, cell, or run boundaries.

{%p if include_signature_block %}
Signed at {{ city }} on {{ signing_date }}.
{%p endif %}

The paragraphs containing {%p if ... %} and {%p endif %} are removed by docxtpl. Only the content in between remains when the condition is true.

For inserted DOCX subdocuments or paragraph-level content, the placeholder must also stand alone in its own paragraph:

{{p template }}

Rules of Thumb

  • Put {%p ... %}, {%tr ... %}, {%tc ... %}, {%r ... %}, and {{p ... }} on their own paragraph, row, cell, or run.
  • Do not write both the opening and closing prefixed tags in the same paragraph or row. Use separate Word paragraphs/rows for the start and end tags.
  • Keep spaces inside the delimiters: write {%p if condition %}, not {%pif condition%}.
  • Style the real content paragraphs, rows, and cells, not the placeholder paragraph or row that contains the control tag, because that wrapper is removed during rendering.
  • In bulleted or numbered lists, put {%p if ... %} and {%p endif %} on their own list paragraphs. This avoids list numbering and missing-endif problems.
  • If Word splits a tag across multiple runs because of formatting changes, delete the whole tag and retype it as plain contiguous text.

{{r ... }} and Rich Text

Use {{r ... }} when the value is a docxtpl.RichText object or another helper returns rich inline content. This is different from a normal {{ variable }} insertion:

  • {{ variable }} keeps the current run styling from the template
  • {{r value }} lets the value carry its own inline formatting

This also applies to built-in filters that produce rich DOCX output. For example, in Word templates use {{r client_name | or('Client Name') }} and {{p project_description | markdown }}.

Be careful with run styling:

  • {{r ... }} resets the current character styling unless the RichText value specifies it
  • paragraph style still comes from the surrounding paragraph
  • do not use two {{r ... }} insertions in the same run
  • Jinja filters do not apply cleanly to RichText, so transform the value before passing it into the template

Formatting and Structure

VIBE renders the template content and keeps the surrounding DOCX structure intact, so Word formatting usually comes from the document itself:

  • Paragraph styles, fonts, spacing, and margins stay in the .docx
  • Headings should use Word's built-in heading styles if you want numbering and cross-references to behave correctly
  • Tables, headers, footers, and page layout belong in Word, not in the template logic

For cross-references in DOCX, use Word's native cross-reference tools rather than typing section numbers manually. See Cross-References and Section Numbering.

Components in DOCX

Components work in DOCX templates too:

  • A DOCX host can insert Markdown or DOCX components
  • A Markdown host can also insert DOCX components
  • The same {{ insert(...) }} call is used in both formats

For the component model itself, see Reuse with Components. For data structures reused between components, see Reusable Data with definitions.

DOCX Wrapper

A DOCX wrapper is a shell Word document that provides the "chrome" around your rendered content — cover pages, headers, footers, page layout, and style definitions. Instead of baking all of that into every template, you maintain it once in the wrapper and let VIBE insert the interview output at render time.

This works for both DOCX and Markdown templates:

  • DOCX templates: The rendered DOCX is inserted as a subdoc into the wrapper.
  • Markdown templates: The rendered Markdown is converted to HTML, then to a DOCX subdoc, and inserted into the wrapper. When a wrapper is configured, the download menu offers DOCX as an additional format alongside HTML and PDF.

How it works

  1. A template opts in by declaring docx_wrapper in its config.yml with a filename pattern.
  2. At render time, VIBE resolves the filename pattern against the interview state (session context variables, user answers, computables).
  3. The wrapper file is opened and the rendered interview content is inserted as a subdoc at the {{p template }} placeholder.
  4. If any step fails (no path configured, missing variable, file not found), VIBE falls back to returning the unwrapped output and logs a warning.

Configuring a wrapper

The system-level config.yml tells VIBE where wrapper files live on disk:

# config.yml (system)
docx_wrapper:
  path: /data/wrapper_files

Each template opts in with its own config.yml:

# templates/my-template/config.yml
docx_wrapper:
  filename: "{organization_id}.docx"

The filename value is a Python format string resolved against the interview state. Any top-level variable available in the interview can be used (e.g. {organization_id}, {client_id}).

A template can also provide its own path, which takes priority over the system-level one. Relative paths resolve against the template's bundle directory:

docx_wrapper:
  path: ../shared_wrappers    # relative to this template's directory
  filename: wrapper.docx

For the full list of configuration keys and fallback behavior, see DOCX Wrapper configuration.

Authoring a wrapper file

The wrapper is a normal .docx file with one special requirement: it must contain a {{p template }} placeholder paragraph where the rendered content will be inserted. Everything else in the wrapper — cover page, headers, footers, page margins, style definitions — is preserved as-is.

The wrapper is rendered as a Jinja template with access to the full interview state, so you can use Jinja expressions in the wrapper itself. This is useful for dynamic headers, footers, and cover pages:

{# In a wrapper header or cover page: #}
{{ upphandling.avtal.objekt.beskrivning }}
{% if defined("upphandling") %}
Avtal {{ upphandling.avtal.id }}
{% endif %}

Since the wrapper may be shared across different templates or systems (e.g. both VIBE and docassemble), guard template-specific variables with {% if defined("variable_name") %} to avoid errors when a variable doesn't exist in all contexts.

Style mapping

When converting Markdown to DOCX, the converter uses standard English style names (Heading 1, Body Text, List Bullet, etc.). If the host document uses different style names, style_map remaps them. The mapping applies to all Markdown-to-DOCX conversions: wrapper rendering, the | markdown Jinja filter, and assistant block output.

docx_wrapper:
  filename: wrapper.docx
  style_map:
    Heading 1: Title
    Heading 2: Heading 1
    Heading 3: Heading 2
    Body Text: Brödtext 2
    Quote: Citat
    List Bullet: Lista (a)
    List Number: Lista (1)

Keys are the converter's default style names; values are the document's style names. Unmapped styles pass through unchanged — Word resolves standard style names across locales automatically, so mapping is only needed when the document uses non-standard names or when heading levels need shifting.

The converter's default style names are:

HTML element Default style Notes
<h1><h9> Heading 1Heading 9
<p> Body Text Has paragraph spacing unlike Normal
<blockquote> Quote Applied to paragraphs inside blockquotes
<ul><li> List Bullet Nested: List Bullet 2, List Bullet 3
<ol><li> List Number Nested: List Number 2, List Number 3
<pre><code> (none) Monospace font with grey shading
<code> (inline) (none) Monospace font on the run
<table> (none) Standard DOCX table

Debugging wrappers

When running in development mode (devel: true), VIBE saves an intermediate copy of the rendered subdoc to the docx_wrapper work directory (under the data directory) before inserting it into the wrapper. This lets you open the subdoc separately to isolate whether a rendering issue is in the template content or the wrapper.

Use --format docx with vibe test to generate DOCX output from Markdown templates:

vibe test my-template -s "My scenario" --save . --format docx

Common Gotchas

Tags split by formatting

If Word splits a tag into multiple styled fragments, the template can become fragile. If a variable or control tag behaves strangely, delete the whole tag and retype it as plain contiguous text.

Manual numbering

Do not type section numbers by hand if they may shift. Use Word heading numbering and Word cross-references instead. VIBE updates those references after rendering.

Logic-heavy templates

DOCX is best when the presentation is important. If the template logic becomes hard to reason about, move calculations into computable variables and shared content into components.

Preview mismatch

The browser preview is useful, but the DOCX output is the source of truth for Word formatting. Check the actual .docx output when debugging spacing, numbering, or layout issues.

Debugging DOCX Templates

Start with the same checks you would use for Markdown templates:

  • Validate the template with vibe validate
  • Confirm question visibility and answers with the interview preview
  • Reduce the problem to the smallest failing section you can

For DOCX-specific debugging, the vibe-dev docx commands are the most useful:

vibe-dev docx read path/to/output.docx
vibe-dev docx component-render my-template
vibe-dev docx insert-debug my-template
vibe-dev docx download-debug my-template

The command reference is in vibe-dev - VIBE Developer Toolkit.

Workflow Suggestions

For a Word-first workflow:

  1. Start from a clean .docx with the styles, numbering, and boilerplate you want.
  2. Add Jinja tags gradually and keep them visually simple.
  3. Validate often with vibe validate.
  4. Render early to catch numbering, bookmark, and layout issues while the template is still small.
  5. Extract repeated sections into components once the structure stabilizes.

See Also