Technical4 April 202610 min readFounder

One JSON Payload, Every Enterprise Document You'll Ever Need

A look at what VaultPDF's rendering pipeline actually does, from the .vpdf format and hierarchical tables to signature zones, supplementary annexes, and audit-grade output.

The previous post covered why we built VaultPDF the way we did: rendering inside your Azure subscription, no data egress, and a no-egress architecture that keeps compliance teams comfortable. This one is about what it actually produces.

I'll use real examples from the feature set we've built over the past two years working with enterprise customers. Some of this will be familiar if you've read the docs. Some of it is context that doesn't fit neatly into a reference doc.


The .vpdf Format

Everything starts with a .vpdf file. It's a JSON file with a .vpdf extension, no binary encoding, no proprietary format. Open it in any text editor. Commit it to Git. The structure is:

{
  "templateId": "site-inspection-report-v3",
  "metadata": {
    "displayName": "Site Inspection Report v3",
    "version": "3.1.0",
    "author": "[email protected]",
    "lastModified": "2026-01-10"
  },
  "theme": {
    "primaryColor": "#0D3B66",
    "accentColor": "#0077B6",
    "fontFamily": "Roboto"
  },
  "templateSettings": {
    "locale": "en-GB",
    "dateFormat": "DD MMM YYYY",
    "timezone": "Europe/London"
  },
  "layoutSchema": { "...": "nodes that define what appears in the document" },
  "hierarchyRules": { "...": "per-level styling and field rules for nested tables" },
  "sampleData": { "...": "used for Designer preview only, ignored at render time" }
}

When you render a document, you merge your live runtime data at the top level alongside the template keys and POST it to your VaultPDF Compute Node endpoint. The sampleData block is stripped before rendering and only exists for the Designer's live preview.

Version control is straightforward. Because templates are JSON files, you can diff them, branch them, and roll back exactly the way you would any other code artifact.


What a Payload Actually Looks Like

The runtime payload combines the template definition and live data in a single JSON object:

{
  "templateId": "site-inspection-report-v3",
  "theme": { "primaryColor": "#0D3B66" },
  "templateSettings": {
    "locale": "en-GB",
    "dateFormat": "DD MMM YYYY",
    "timezone": "Europe/London"
  },
  "watermarkText": "DRAFT",
  "project": {
    "name": "Tower Block A - Cladding Replacement",
    "reference": "PRJ-2026-0441",
    "siteAddress": "14 Commercial Road, London E1 6LT"
  },
  "inspector": {
    "name": "James Whitfield",
    "signatureRef": "signatures/jwhitfield.png"
  },
  "inspectionZones": [
    {
      "zone": "Level 1 - West Face",
      "items": [
        {
          "itemRef": "WF-001",
          "description": "Panel alignment check",
          "status": "Pass",
          "notes": ["Alignment within 2mm tolerance", "No fixings loose"],
          "images": ["photos/wf001-a.jpg", "photos/wf001-b.jpg"]
        }
      ]
    }
  ]
}

The REFRACT Engine reads the fixed top-level keys (theme, templateSettings, watermarkText, layoutSchema, etc.) and treats everything else as data available for binding. No schema registration. No field mapping step.


Hierarchical Tables: The Hard Part We Got Right

Most document generators handle flat lists fine. The moment your data has nesting, orders with line items, line items with serial numbers, serial numbers with inspection records, things get messy fast.

Hierarchical Tables in VaultPDF handle this as a first-class feature. You bind one table block to a top-level array, and the renderer recursively processes each nesting level. You control each level independently via hierarchyRules:

{
  "hierarchyRules": {
    "fieldVisibility": {
      "internalCode": false,
      "adjustedQty": "level >= 3 && adjustedQty > 0"
    },
    "levelStyling": {
      "2": {
        "fillColor": "#F0F4F8",
        "indentPt": 16,
        "fontSize": 9
      },
      "3": {
        "fillColor": "#E8EFF5",
        "indentPt": 32,
        "fontSize": 8,
        "italics": true
      }
    },
    "computations": [
      {
        "targetField": "lineTotal",
        "expression": "qty * unitPrice",
        "format": "currency"
      },
      {
        "targetField": "subtotal",
        "aggregation": "sum",
        "sourceField": "lineTotal",
        "scope": "children"
      }
    ]
  }
}

fieldVisibility accepts literal booleans or JavaScript expression strings, evaluated per row with the row's data in scope. levelStyling applies per depth level, so your level-2 rows look different from level-3 rows without any conditional logic in the template. computations derive fields that don't exist in the payload: totals, formatted values, and conditional labels.

This is the part of the product that took the longest to get right. Page break behaviour across deeply nested tables is genuinely hard. The REFRACT Engine handles widow/orphan control per level and keeps parent header rows visible when a level-3 block crosses a page boundary.


Structured Content: Long-Form Documents Without Chaos

For long-form documents, service agreements, terms and conditions, and policy documents, you don't want to embed clause text directly in a template. Terms change. Clauses get updated for different jurisdictions. You don't want a developer involved every time.

Structured Content lets you author those documents as separate YAML assets stored in SharePoint:

documentType: terms-and-conditions
version: "4.2.0"
title: "Master Services Agreement"
sections:
  - heading: "1. Services"
    blocks:
      - type: paragraph
        text: "VaultPDF will provide the Services described in the applicable Order Form..."
      - type: paragraph
        text: "Services are subject to the acceptable use policy at vaultpdf.io/legal/aup"
        condition: "tier === 'enterprise'"
  - heading: "2. Data Handling"
    blocks:
      - type: paragraph
        text: "All Customer Data remains within the Customer's Azure subscription at all times."

The condition field on any block takes a JavaScript expression evaluated against the conditionContext you pass at render time. Different conditions for different tiers, regions, or document types, same asset file, same template, different output.

At render time The REFRACT Engine pre-fetches the asset from SharePoint asynchronously, converts it to the PDF layout, and injects it in place of the structured_content node. Table of Contents entries and section numbers are generated automatically.


Supplementary Annex: Per-Item Evidence Sections

This one came directly from a construction customer. They needed inspection reports where every qualifying line item had its own section at the end of the document: a header showing the item reference, the QC notes for that item, and a grid of site photos.

The Supplementary Annex block handles this automatically. You add it to your template once:

{
  "id": "supplementary-annex",
  "type": "structured_content",
  "props": {
    "assetPath": "Documents/SupplementaryAnnex/inspection-annex-layout.yaml",
    "library": "assets"
  },
  "pagination": { "breakBefore": "always" }
}

And you control it from the payload with a single flag:

{
  "features": {
    "supplementaryAnnex": true
  }
}

When supplementaryAnnex is true, the renderer iterates every line item, finds items with notes or image references, and generates a standalone section for each: themed header, notes paragraph, and photo grid. Images are pre-fetched and downsampled to the configured DPI. When the flag is absent or false, the section is silently omitted, no placeholder, no empty page, no Table of Contents ghost entry.


Signature Zones, Approval Grids, and Verification

For contracts and agreements, the template reserves physical space for signatures using Signature Zones, areas positioned precisely on the page that are compatible with DocuSign, Adobe Sign, and Dropbox Sign. The zone is a transparent overlay area in the PDF that signature platforms can detect and populate.

The Approval Workflow block renders a table of signers sourced from the payload: name, role, date, and an "awaiting signature" state indicator. It's used for documents that circulate for review before being signed.

Document verification uses two mechanisms applied to the final output:

{
  "templateSettings": {
    "verification": {
      "qrCode": true,
      "qrBaseUrl": "https://verify.vaultpdf.io",
      "fingerprint": true
    }
  }
}

The QR code encodes a verification URL containing the document ID. Anyone who receives the PDF can scan it and confirm the document is authentic. The DJB2 fingerprint is a hash of the rendered document content, stored in the audit log and printed on the document. If two fingerprints match, the documents are bit-for-bit identical.


Template Settings: The Formatting Layer

One thing that saves a disproportionate amount of time in practice is the global formatting layer. You declare locale, date format, currency, and number format once at the template level, and every date, number, and currency value in every table across the entire document respects it:

{
  "templateSettings": {
    "timezone": "Europe/London",
    "locale": "en-GB",
    "dateFormat": "DD MMM YYYY",
    "currency": {
      "symbol": "£",
      "symbolPosition": "before",
      "decimalPlaces": 2,
      "thousandsSeparator": ","
    },
    "toc": {
      "enabled": true,
      "title": "Contents",
      "maxDepth": 3
    },
    "sectionNumbering": {
      "enabled": true,
      "style": "decimal"
    }
  }
}

No per-column format strings. No formatter functions in the template code. Change locale to "de-DE" and every number, date, and currency in the output shifts to German conventions automatically.


The Audit Trail

Every render writes a structured event to the tenant's audit log:

{
  "eventType": "render_success",
  "documentId": "doc_2026_0441_abc123",
  "templateId": "site-inspection-report-v3",
  "templateVersion": "3.1.0",
  "triggeredBy": "[email protected]",
  "payloadHash": "sha256:8f3b2c...",
  "generatedAt": "2026-01-22T09:14:33.412Z",
  "deliveryTarget": "SharePoint: /sites/Projects/Generated Documents/2026/01/PRJ-2026-0441.pdf",
  "processingMs": 847
}

Three tiers for different volumes: basic (SharePoint JSONL, suitable for low volume), blob (Azure Blob Storage AppendBlob, medium volume), and enterprise (Azure Service Bus queues feeding an Audit Worker with guaranteed delivery, high volume). The schema is identical across all three. You can switch tiers by changing one line in the storage config without touching any templates or flows.


Where Things Stand

The features described here are all in production. We're actively working on a few things that aren't yet in the docs:

  • A change history UI in the portal for template versions, with visual diff between .vpdf snapshots
  • Email delivery as a native delivery target alongside SharePoint and Azure Blob Storage
  • Expanded Power Platform connector actions for triggering renders directly from model-driven apps without a cloud flow

If you want to see any of this applied to a real document type you're working with, drop us a message. We're genuinely interested in the hard cases, the documents that have broken every other tool you've tried.

[email protected]