Skip to content
Orbit GroundControl home
Orbit GroundControl home

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:

  1. Open Orbit MissionControl.

  2. Navigate to Settings > Document Templates.

  3. 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

code128

Code 128

IDs, tracking numbers, labels

{{ tour.id \| code128 }}

qrcode

QR Code

URLs, longer text, mobile scanning

{{ "<https://example.com>" \| qrcode }}

datamatrix

Data Matrix

Compact data, industrial use

{{ load.id \| datamatrix }}

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: always

Forces an explicit page break after each element.

Both rules work together:

  • @page tells the PDF engine the size of each page

  • page-break-after tells it where to start a new page

Without page-break-after, content would only break when it overflows the page size. With loops (like {% for load in tour.loads %}), you need page-break-after: always to ensure each iteration starts on a new page.

How the Preview Works:

Since browsers only apply @page rules in print mode (not in normal screen rendering), the editor simulates multi-page layouts by scanning your <style> blocks for page-break-after: always. When found:

  1. The body transforms into a flex container with gaps

  2. Each matching element renders as a separate paper sheet with a shadow

  3. 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

  1. Navigate to the Document Templates list in Settings.

  2. Click the Test Render button (Play Icon) next to your template.

  3. A dialog opens where you can select real data from your system (e.g., an actual Tour, Order, or Shipment).

  4. Click Generate to render the template with the selected data.

  5. 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

code128

Generates a Code 128 barcode (SVG).

~2,000 chars

{{ tour.id \| code128 }}

qrcode

Generates a QR Code (SVG).

~4,000 chars

{{ "<https://orbit.com>" \| qrcode }}

datamatrix

Generates a Data Matrix code (SVG).

~2,000 chars

{{ load.id \| datamatrix }}

formatDate

Formats a Unix timestamp. Locales: 'de' (DD.MM.YYYY), 'en' (YYYY-MM-DD).

{{ tour.latestStart \| formatDate: 'de' }}

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

Tour with stops, loads, timing, etc.

Order

order

Order details

Shipment

shipment

Shipment information

Carrier

carrier

Carrier company data

Carrier User

carrierUser

Individual carrier user

Carrier Team

carrierTeam

Carrier team information

Multi-Page CSS Properties

Property

Description

page-break-after: always

Forces a page break after the element

page-break-after: auto

Use on :last-child to prevent trailing empty page

Troubleshooting

Pages not separating in preview?

  1. Ensure page-break-after: always is in a <style> block (not inline)

  2. Check that the CSS selector matches your page elements

  3. 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?

  1. Check that you selected the correct Data Object in the toolbar

  2. Verify the variable path exists (e.g., tour.stops not tour.stop)

  3. Use {% if variable %} to handle optional fields

External Documentation