Document Engine
Overview
The Orbit Document Engine allows you to generate PDF documents dynamically based on your operational data. It combines standard web technologies (HTML & CSS) with the Liquid templating language. Instead of relying on rigid, pre-defined templates, the Document Engine gives you control over the layout, styling, and content of your documents. Whether you need shipping labels, CMRs, invoices, or custom reports, you can design them exactly to your specifications.
Getting Started
Accessing the Document Engine
To start creating or editing templates:
Open Orbit MissionControl.
Navigate to Settings > Document Templates.
Click Create Template or select an existing one.
Define Data Objects
Data Objects define the type of object the template can access. Each Data Object you add to a template must be provided when rendering. For example, if you define a template with a "Tour" Data Object, you must provide a tour ID when rendering the template.
In the editor toolbar, use the "Data Objects" dropdown to select your source data (e.g., Tour, Order). This enables the correct Liquid variables and loads the corresponding mock data for the preview.
Template Content
Inside the template content editor, you can use standard HTML and CSS embedded in style tags to define the document layout. This includes all HTML properties supported by modern web browsers.
In addition to HTML, the template content uses the Liquid templating language to access specialized functions, data objects, and control flow primitives. The examples below demonstrate how to use Liquid. For a more detailed reference on the Liquid templating language, see this link.
To control document size and page setup for printing, we recommend using the CSS paged media module. This module provides specialized CSS directives to control page size, margins, and page breaks. The examples below cover these properties. For a more detailed reference, see this link.
Live Preview
The Live Preview allows you to test your layout instantly using mock data. As you type, the preview updates in real-time.
Walkthrough Part 1: Single Page Label
In this first step, we will create a simple shipping label with a physical label size of 100mm x 150mm.
Copy this into the editor:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Shipping Label</title> <style> /* 1. Define Page Size */ @page { size: 100mm 150mm; margin: 0; } body { font-family: Arial, sans-serif; margin: 0; padding: 0; } /* 2. Define the container for our content */ .label-page { width: 100mm; height: 150mm; padding: 5mm; box-sizing: border-box; display: flex; flex-direction: column; border: 1px dashed #ccc; } .section { border-bottom: 2px solid black; padding-bottom: 5px; margin-bottom: 5px; } .barcode-container { text-align: center; margin-top: auto; } .barcode-container svg { width: 80mm; height: 15mm; } </style> </head> <body> <!-- 3. Label Content --> <div class="label-page"> <!-- Inject Addresses --> {% assign sender = tour.stops | first %} {% assign recipient = tour.stops | last %} <div class="section"> <strong>FROM:</strong><br /> {{ sender.address.companyName }}<br /> {{ sender.address.street }} {{ sender.address.houseNumber }}<br /> {{ sender.address.zipCode }} {{ sender.address.city }} </div> <div class="section"> <strong>TO:</strong><br /> <h2 style="margin: 0;">{{ recipient.address.companyName }}</h2> {{ recipient.address.street }} {{ recipient.address.houseNumber }}<br /> {{ recipient.address.zipCode }} {{ recipient.address.city }} </div> <!-- Barcode (Tour ID) --> <div class="barcode-container">{{ tour.id | code128 }}</div> </div> </body> </html>
Adding Barcodes
In the example above, we used {{ tour.id | code128 }} to generate a barcode. Barcodes are created using Liquid filters that convert any string into an SVG image.
Available Barcode Types:
Filter | Type | Best For | Example |
|---|---|---|---|
| Code 128 | IDs, tracking numbers, labels |
|
| QR Code | URLs, longer text, mobile scanning |
|
| Data Matrix | Compact data, industrial use |
|
Usage Example:
<!-- Code 128 Barcode --> <div class="barcode-container">{{ tour.id | code128 }}</div> <!-- QR Code --> <div class="qr-container"> {{ "<https://track.example.com/>" | append: tour.id | qrcode }} </div> <!-- Data Matrix --> <div class="matrix-container">{{ load.id | datamatrix }}</div>
Tip: All barcode filters output inline SVG, so you can style them with CSS (width, height, etc.).
Walkthrough Part 2: Multi-Page Labels
Now, let's say we want one label per load (e.g., 5 pallets = 5 pages).
Important: A Load object can have a count greater than one. This means multiple units of the same Load (with identical dimensions and weight as defined in the Load object) need to be handled. The example below does not account for load count. While it's possible to generate one label per unit, we don't recommend this approach for label generation. Instead, ensure the count is always one and create one label per Load object.
1. Enable Multi-Page Support
To create multiple pages in the PDF and preview, use the page-break-after CSS rule in combination with a Liquid loop.
Update your CSS:
Add page-break-after: always; to your .label-page class:
.label-page { /* ... existing styles ... */ /* IMPORTANT: This triggers the page break */ page-break-after: always; } /* Optional: Prevent empty page at the end */ .label-page:last-child { page-break-after: auto; }
Understanding @page vs page-break-after:
CSS Rule
Purpose
@page { size: 100mm 150mm; }Defines the page dimensions for the PDF.
page-break-after: alwaysForces an explicit page break after each element.
Both rules work together:
@pagetells the PDF engine the size of each page
page-break-aftertells it where to start a new pageWithout
page-break-after, content would only break when it overflows the page size. With loops (like{% for load in tour.loads %}), you needpage-break-after: alwaysto ensure each iteration starts on a new page.
How the Preview Works:
Since browsers only apply
@pagerules in print mode (not in normal screen rendering), the editor simulates multi-page layouts by scanning your<style>blocks forpage-break-after: always. When found:
The body transforms into a flex container with gaps
Each matching element renders as a separate paper sheet with a shadow
Visual spacing (24px) appears between pages
This detection happens automatically—you don't need to add any special classes or markup beyond the CSS rule.
2. Loop Through Loads
Now wrap your content in a Liquid loop to generate one page (and thus one label) for every load.
Complete Multi-Page Example:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Shipping Labels - {{ tour.id }}</title> <style> @page { size: 100mm 150mm; margin: 0; } body { font-family: Arial, sans-serif; margin: 0; padding: 0; } .label-page { width: 100mm; height: 150mm; padding: 5mm; box-sizing: border-box; display: flex; flex-direction: column; border: 1px dashed #ccc; page-break-after: always; } .label-page:last-child { page-break-after: auto; } .header { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid black; padding-bottom: 3mm; margin-bottom: 3mm; } .load-badge { background: #333; color: white; padding: 2mm 4mm; font-weight: bold; font-size: 14px; } .section { border-bottom: 1px solid #ccc; padding-bottom: 3mm; margin-bottom: 3mm; } .recipient { flex: 1; } .recipient h2 { margin: 0 0 2mm 0; font-size: 18px; } .load-info { background: #f5f5f5; padding: 3mm; margin-bottom: 3mm; } .barcode-container { text-align: center; margin-top: auto; padding-top: 3mm; border-top: 2px solid black; } .barcode-container svg { width: 80mm; height: 12mm; } .barcode-text { font-family: monospace; font-size: 10px; margin-top: 1mm; } </style> </head> <body> {% assign sender = tour.stops | first %} {% assign recipient = tour.stops | last %} <!-- Loop: One label per load --> {% for load in tour.loads %} <div class="label-page"> <div class="header"> <div class="load-badge"> {{ forloop.index }} / {{ tour.loads.size }} </div> <div>{{ tour.latestStart | formatDate: "en" }}</div> </div> <div class="section"> <strong>FROM:</strong><br /> {{ sender.address.companyName }}<br /> {{ sender.address.city }} </div> <div class="recipient"> <strong>TO:</strong> <h2>{{ recipient.address.companyName }}</h2> {{ recipient.address.street }} {{ recipient.address.houseNumber }}<br /> {{ recipient.address.zipCode }} {{ recipient.address.city }} </div> <div class="load-info"> <strong>Load:</strong> {{ load.type }}<br /> <strong>Quantity:</strong> {{ load.count }}<br /> <strong>Weight:</strong> {{ load.totalWeight }} kg </div> <div class="barcode-container">{{ load.id | code128 }}</div> </div> {% endfor %} </body> </html>
You should now see multiple labels in the preview, separated by a gap.
Testing & Verification
Once you are satisfied with your layout, you can test your template with real world data from within your Orbit account.
Test Render
Navigate to the Document Templates list in Settings.
Click the Test Render button (Play Icon) next to your template.
A dialog opens where you can select real data from your system (e.g., an actual Tour, Order, or Shipment).
Click Generate to render the template with the selected data.
The generated PDF will be downloaded automatically to your device.
This allows you to verify that your template works correctly with production data before using it in your workflows.
Rendering Documents
The primary way to integrate document rendering into your workflow is through automations. While our upcoming Orbit Automations feature will soon provide native, first-class support for rendering documents directly within the platform, this functionality is currently in development.
Until Orbit Automations is released, the only way to render documents is programmatically via the Orbit API.
To bridge this gap today, we recommend using a third-party automation provider (such as n8n) to combine Orbit Webhooks with the Document Templates API. This allows you to react to events and trigger renders automatically. If you need assistance setting this up, or would prefer us to manage these automations for you, please reach out to Orbit support.
Reference
Custom Filters
Filter | Description | Max Length | Example |
|---|---|---|---|
| Generates a Code 128 barcode (SVG). | ~2,000 chars |
|
| Generates a QR Code (SVG). | ~4,000 chars |
|
| Generates a Data Matrix code (SVG). | ~2,000 chars |
|
| Formats a Unix timestamp. Locales: | — |
|
Note: If barcode content exceeds the maximum length, an error placeholder is rendered instead of crashing.
Available Data Objects
Data Object | Template Variable | Description |
|---|---|---|
Tour |
| Tour with stops, loads, timing, etc. |
Order |
| Order details |
Shipment |
| Shipment information |
Carrier |
| Carrier company data |
Carrier User |
| Individual carrier user |
Carrier Team |
| Carrier team information |
Multi-Page CSS Properties
Property | Description |
|---|---|
| Forces a page break after the element |
| Use on |
Troubleshooting
Pages not separating in preview?
Ensure
page-break-after: alwaysis in a<style>block (not inline)Check that the CSS selector matches your page elements
The rule must be exactly
page-break-after: always
Barcode showing error?
The content is too long. Code128 supports ~2,000 characters, QR codes ~4,000.
Variables not rendering?
Check that you selected the correct Data Object in the toolbar
Verify the variable path exists (e.g.,
tour.stopsnottour.stop)Use
{% if variable %}to handle optional fields
External Documentation
LiquidJS: For syntax, loops, and standard filters.
CSS Paged Media: For controlling page size and breaks.