# VeloxFactory

# The Basics

What VeloxFactory is, how it is installed and configured, and a first look at the frontend and the API.

# What is VeloxFactory?

<div style="text-align: justify;">

VeloxFactory is a web application that lets you manage and render JasperReports templates - via a clean frontend, a powerful REST API, or both. It turns the well-established JasperReports engine into something any application can consume with a single HTTP call.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
🖥️ <strong>Built for the desk, usable on the move.</strong> VeloxFactory's views are optimised for desktop browsers, a resolution of <strong>1600×900</strong> or more gives the most comfortable working experience. The pages work on tablets and phones as well, which is what Scan2Print is built for: a scan station is a mobile device in the warehouse, not a workstation.
</div>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.index.png</code><br>Report Config overview, the landing page after login, with context badges and thumbnails.
</div>

---

<h3 id="where-it-fits" style="color: #203671; margin-top: 2.2em;">Where VeloxFactory Fits</h3>

JasperReports is a mature, battle-tested report engine. It produces high-quality PDFs, supports complex layouts, barcodes, images, and dynamic data - and it has been doing so reliably for decades. Integrating it directly requires a JVM on your server and Java code to drive it - which is a natural fit for Java stacks, but an overhead most PHP, Python, or C# teams prefer to avoid.

VeloxFactory sits on top of the JasperReports render engine and exposes its capabilities via a clean REST API and a management frontend. The render engine runs in pure PHP - no JVM process required on your application server. You design your report templates in Jaspersoft Studio as usual, upload them once, and from that point on, rendering is just a POST request.

```http
POST /api/v1/report-config/A5_KanBan/render
Content-Type: application/json
Authorization: Bearer <token>

{
  "outputType": "base64",
  "parameters": { "P_ARTICLE_NUMBER": "4561287-154" },
  "data": [...],
  "createHistoryRecord": true
}
```

That is the entire integration.

---

<h3 id="origin" style="color: #203671; margin-top: 2.2em;">Where It Came From</h3>

VeloxFactory was built by someone with a logistics and IT background who needed a simple, reliable way to generate article labels on demand - from whatever system was running at the time, without caring about the underlying report engine. The original idea was modest: a small API wrapper around JasperReports, nothing more.

It grew. A frontend to manage templates. Connection configs for live SQL data. History records for traceability. Print task dispatching, printer master data, a scan station. Mail delivery with its own mailers, templates and dispatch queue. A job scheduler, so the recurring work runs itself. A permission system. What started as "just get me a label PDF" is now a full suite, capable of enterprise integration on demand.

---

<h3 id="what-it-does" style="color: #203671; margin-top: 2.2em;">What VeloxFactory Does</h3>

At its core, VeloxFactory does six things:

**Manages report templates.** You upload `.jrxml` files, and VeloxFactory analyses them automatically, detecting parameters, data fields, and image resources. Everything is stored, versioned, and ready to render.

**Connects to your data.** VeloxFactory can execute SQL queries against live databases at render time. MySQL, MariaDB, PostgreSQL, and SQL Server are all supported. No live connection needed either - you can deliver data directly in the render request as a JSON array, which makes it equally useful for applications that already have the data in memory.

**Renders on demand.** A single API call produces a PDF. You choose the output format - Base64-encoded inline, a file URL, or silent output for print-only flows. Rendering is synchronous and fast.

**Dispatches print jobs.** Rendered PDFs can be forwarded directly to a print service via WebSocket, creating a traceable `ReportPrintTask` with status tracking. Labels off the printer, not just on the screen. Printers are maintained as master data and picked by name, and with **Scan2Print** a barcode scan on a phone or a handheld is enough to turn a scanned value into a printed label.

**Delivers by mail.** A rendering can be sent straight to whoever needs it. A render request carries an optional `mailing` segment, and VeloxFactory sends an HTML mail through a configured mailer, using a configured template, with the PDF attached and, on request, an xlsx export of the same data. Sending is synchronous or queued, every mail is logged as a traceable `ReportMailTask`.

**Works on a schedule.** Not every document has somebody waiting for it. The shift report that belongs in a mailbox at six in the morning, the weekly stock list, the cleanup that keeps the history from growing without bound: recurring work is configured as a schedule with a crontab expression, maintained in the frontend or through the API like everything else. A scheduled render is a complete render request, printing and mailing included, and every run is logged with its trace id.

---

<h3 id="whos-its-for" style="color: #203671; margin-top: 2.2em;">Who It Is For</h3>

VeloxFactory is for anyone who needs to generate documents - labels, reports, delivery notes, production sheets, certificates - reliably and programmatically. The typical home is in **logistics, production, and warehousing**, where printing article labels, kanban cards, or shipment documents is a daily operational need and downtime is not an option.

More broadly: if your application needs to produce a PDF from structured data, and you do not want to build and maintain a report engine yourself, VeloxFactory is the answer.

---

<h3 id="deployment" style="color: #203671; margin-top: 2.2em;">Deployment</h3>

VeloxFactory is a standard Laravel application. It runs wherever PHP runs, which is nearly everywhere.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px;">Scenario</th>
      <th style="text-align: left; padding: 6px 10px;">Works?</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Cloud VM or VPS</td>
      <td style="padding: 6px 10px;"><span style="color: #349b31; font-weight: 600;">Yes</span></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">On-premise server</td>
      <td style="padding: 6px 10px;"><span style="color: #349b31; font-weight: 600;">Yes</span></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Docker container</td>
      <td style="padding: 6px 10px;"><span style="color: #349b31; font-weight: 600;">Yes</span></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Raspberry Pi on the shop floor</td>
      <td style="padding: 6px 10px;"><span style="color: #349b31; font-weight: 600;">Yes</span></td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;">Jaspersoft Server</td>
      <td style="padding: 6px 10px;"><span style="color: #525E5A;">Not needed</span></td>
    </tr>
  </tbody>
</table>

The setup is intentionally slim. PHP, a database for VeloxFactory itself, and the JasperPHP render engine, that is the stack. No application server, no JVM process to manage, no separate Jaspersoft infrastructure. VeloxFactory bundles everything it needs.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Want to try it first?</strong> A live demo is available at <a href="https://demo.veloxfactory.dev" target="_blank">demo.veloxfactory.dev</a>.
<p style="margin: 6px 0 0 0;"><strong>Login:</strong> <code>demo@veloxfactory.dev</code> &nbsp;·&nbsp; <strong>Password:</strong> <code>demo</code></p>
</div>

</div>

# Our Vision

<div style="text-align: justify;">

VeloxFactory exists because good software should not be complicated, expensive, or opaque. This page explains what we are building towards and how we think about working with the people who use it.

---

<h3 id="simple-by-design" style="color: #203671; margin-top: 2.2em;">Simple by Design</h3>

The goal behind VeloxFactory has always been the same: take a powerful but complex technology and make it accessible to teams who just need it to work. Not teams with a dedicated Java architect. Not teams with a six-figure infrastructure budget. Teams with a problem to solve and a deadline to meet.

That philosophy shapes every decision in VeloxFactory, from the API design to the frontend workflows. Concepts should be easy to grasp. Setup should be fast. Day-to-day use should feel obvious, even for someone who has never heard of JasperReports.

---

<h3 id="reliable-and-performant" style="color: #203671; margin-top: 2.2em;">Reliable and Performant</h3>

Simple does not mean limited. VeloxFactory is designed to handle real production workloads - in logistics, on the shop floor, in warehouses where a label printer needs to respond in milliseconds and cannot afford to fail during a shift.

The architecture reflects this. The stack is intentionally slim - no unnecessary layers, no bloat. Rendering is synchronous and fast. The application runs comfortably on modest hardware, including single-board computers deployed directly in production environments.

When VeloxFactory is running, it runs. That is the expectation we build to.

---

<h3 id="no-lock-in" style="color: #203671; margin-top: 2.2em;">No Lock-In</h3>

VeloxFactory is built exclusively on open-source components. There is no proprietary cloud dependency, no mandatory subscription, no remote kill switch. You run it where you want - on your own server, in your own cloud account, on a machine on your shop floor - and it stays yours.

The report templates you design in Jaspersoft Studio are standard `.jrxml` files. The database you connect to is your own. The PDFs it produces belong to you. VeloxFactory is infrastructure you own and control, not a service you rent.

---

<h3 id="grows-with-you" style="color: #203671; margin-top: 2.2em;">Grows With You</h3>

Most customers start with a single, focused use case - generating an article label, a delivery note, a production sheet. VeloxFactory handles that out of the box. But as requirements evolve, the platform grows with them.

History records for traceability. Print task dispatching via WebSocket. A scan station for the warehouse floor. Mail delivery with its own templates and dispatch queue. Multi-report management with a structured permission system. API access for any system that can make an HTTP call. None of these need to be in scope on day one, but they are all there when the time comes.

Beyond the built-in capabilities, VeloxFactory is extended on request. If a workflow requires something specific, it can be built in. Licenses include continued development and updates, the software does not freeze at the point of purchase.

---

<h3 id="a-real-partner" style="color: #203671; margin-top: 2.2em;">A Real Partner</h3>

VeloxFactory is not sold by a faceless vendor with a tiered support portal. It is built and maintained by someone with deep hands-on experience in logistics IT, system integration, and shop floor environments, and that experience is available directly to every customer.

This means practical help where it matters: rolling out the application, connecting it to existing ERP or WMS systems, adapting report templates to operational realities, and making sure the implementation actually works in the environment it needs to run in, not just in a demo.

The goal is not a one-time sale. It is a long-term working relationship with customers who have real problems and need a partner who understands them.

---

<h3 id="the-full-picture" style="color: #203671; margin-top: 2.2em;">The Full Picture</h3>

To put it plainly: VeloxFactory is powerful software at a reasonable price, built on open foundations, designed to be simple to operate, and backed by someone who will pick up the phone.

If that sounds like what you need, welcome.

</div>

# Installing VeloxFactory

<div style="text-align: justify;">

VeloxFactory is a standard Laravel application. The setup is intentionally slim - pull the repository, install dependencies, configure the environment, migrate the database. A standard installation is up and running in under 30 minutes.

---

<h3 id="what-you-dont-need" style="color: #203671; margin-top: 2.2em;">What You Don't Need</h3>

Before listing what is required, it is worth being explicit about what is not:

- **No Java** - the render engine is pure PHP. No JVM, no Java runtime, no Java SQL drivers.
- **No Node.js / npm / Vite** - the frontend assets are pre-built and bundled with the repository.
- **No Jaspersoft Server** - VeloxFactory is entirely self-contained.
- **No Docker** - optional for the database only, not required for the application itself.

---

<h3 id="system-requirements" style="color: #203671; margin-top: 2.2em;">System Requirements</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Requirement</th>
      <th style="text-align: left; padding: 6px 10px;">Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">PHP >= 8.2</td>
      <td style="padding: 6px 10px;">Required extensions: <code>ctype</code>, <code>curl</code>, <code>fileinfo</code>, <code>filter</code>, <code>hash</code>, <code>mbstring</code>, <code>openssl</code>, <code>pcntl</code>, <code>pdo</code>, <code>pdo_sqlite</code>, <code>posix</code>, <code>redis</code>, <code>session</code>, <code>simplexml</code>, <code>tokenizer</code>, <code>zip</code>, <code>gd</code>, <code>xml</code>. For report database connections, additionally: <code>pdo_mysql</code> (MySQL / MariaDB), <code>pdo_pgsql</code> (PostgreSQL). SQL Server is listed separately below.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Composer</td>
      <td style="padding: 6px 10px;">PHP dependency manager - <a href="https://getcomposer.org" target="_blank">getcomposer.org</a></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">MySQL or MariaDB</td>
      <td style="padding: 6px 10px;">VeloxFactory's own application database. A Docker container works fine if no local instance is available.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Redis</td>
      <td style="padding: 6px 10px;">Required for the queue driver (Laravel Horizon). Install via <code>apt install redis-server</code> or run as a Docker container. The <code>php-redis</code> extension must also be installed: <code>apt install php-redis</code>. See <a href="/books/veloxfactory/page/background-job-processing" target="_blank">Background Job Processing</a> for details.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">poppler-utils</td>
      <td style="padding: 6px 10px;">Provides <code>pdftoppm</code>, used for generating report thumbnails. Install via <code>apt install poppler-utils</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Supervisor</td>
      <td style="padding: 6px 10px;">Keeps the three background processes running: <strong>Horizon</strong> (queue workers), <strong>Reverb</strong> (WebSocket server), and the <strong>Scheduler</strong>. See <a href="/books/veloxfactory/page/background-job-processing" target="_blank">Background Job Processing</a> for the full Supervisor configuration.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">SMTP access</td>
      <td style="padding: 6px 10px;">Only required for report mailing. The server has to reach the SMTP host of each configured mailer, usually on port 587 or 465. Credentials are not set up here, they are maintained as <a href="/books/veloxfactory/page/mailers-master-data-for-your-smtp-accounts">Mailers</a> inside the application.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Apache2 or nginx</td>
      <td style="padding: 6px 10px;">Optional reverse proxy. Not required for local or development setups.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">php-sqlsrv / pdo_sqlsrv</td>
      <td style="padding: 6px 10px;">Only required when connecting to Microsoft SQL Server as a report data source. Not needed otherwise.</td>
    </tr>
  </tbody>
</table>

<h4 style="color: #203671; margin-top: 1.4em;">Bundled Dependencies</h4>

The following components are bundled with VeloxFactory, no separate installation required:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Component</th>
      <th style="text-align: left; padding: 6px 10px;">Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><a href="https://github.com/QuilhaSoft/JasperPHP" target="_blank">JasperPHP</a></td>
      <td style="padding: 6px 10px;">Pure PHP render engine - parses <code>.jrxml</code> and generates PDFs via TCPDF. Installed automatically by Composer.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><a href="https://laravel.com/docs/horizon" target="_blank">Laravel Horizon</a></td>
      <td style="padding: 6px 10px;">Redis-backed queue manager with built-in dashboard. Manages worker pools for thumbnail generation, background mail dispatch and the nightly purge jobs. Installed automatically by Composer.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">PhpSpreadsheet</td>
      <td style="padding: 6px 10px;">Builds the xlsx export that a report mailing can attach next to the PDF. Needs the PHP extensions <code>zip</code>, <code>gd</code> and <code>xml</code>. Installed automatically by Composer.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><a href="https://github.com/opcodesio/log-viewer" target="_blank">Log Viewer</a></td>
      <td style="padding: 6px 10px;">In-browser Laravel log viewer for monitoring application logs.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><a href="https://scribe.knuckles.wtf/" target="_blank">Scribe</a></td>
      <td style="padding: 6px 10px;">Generates the interactive API documentation from source annotations.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><a href="https://github.com/google/material-design-icons/tree/master/variablefont" target="_blank">Material Design Icons</a></td>
      <td style="padding: 6px 10px;">Icon set used throughout the frontend.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><a href="https://ace.c9.io/" target="_blank">Ace Editor</a></td>
      <td style="padding: 6px 10px;">In-browser code editor for writing SQL queries.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><a href="https://github.com/sinister-labs/sinister-labs" target="_blank">Logo & Color Concept</a></td>
      <td style="padding: 6px 10px;">Visual identity by sinister-labs.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Jaspersoft Studio 6.21.5</td>
      <td style="padding: 6px 10px;">Desktop IDE for designing <code>.jrxml</code> report templates. Installed separately on the designer's machine, not on the server. <a href="https://community.jaspersoft.com/download-jaspersoft/community-edition/jaspersoft-studio_windows_6.x" target="_blank">Download here.</a></td>
    </tr>
  </tbody>
</table>


---

<h3 id="first-setup" style="color: #203671; margin-top: 2.2em;">First Setup</h3>

`php artisan setup:base` prepares the storage folders and creates the administrator `admin@veloxfactory.dev`. Its password is generated at random and printed to the console exactly once, it is not stored anywhere in clear text. Copy it down. The first sign-in forces a password change before anything else in the instance is usable.

<h4 id="nothing-below-storage-is-executed" style="color: #203671; margin-top: 1.4em;">Nothing below storage is executed</h4>

Rendered documents, report templates and resource files are served as static files from `storage/app/public`. The web server must never execute anything in that directory. The repository ships `storage/app/public/.htaccess` for Apache; add the matching directive to your virtual host:

```apache
<Directory /var/www/veloxfactory/storage/app/public>
    php_admin_flag engine off
    Options -Indexes -ExecCGI
    AllowOverride All
</Directory>
```

For nginx:

```nginx
location ^~ /storage/ {
    location ~ \.(php|phar|phtml)$ { deny all; }
    add_header X-Content-Type-Options "nosniff" always;
}
```

<h4 id="administrative-routes" style="color: #203671; margin-top: 1.4em;">Administrative routes</h4>

The Log Viewer is reachable at `admin/log-viewer` (configurable through `LOG_VIEWER_ROUTE`) and requires `global:admin`. The API reference generated by Scribe is at `/docs` and requires `global:use-api`.

---

<h3 id="deployment-options" style="color: #203671; margin-top: 2.2em;">Deployment Options</h3>

VeloxFactory can be deployed in two ways: self-hosted on your own infrastructure, or fully managed and operated by kiwi software. Both options deliver identical functionality, the choice depends on your team's operational preferences and existing infrastructure.

<h4 id="self-hosted" style="color: #203671; margin-top: 1.4em;">Self-Hosted</h4>

You run VeloxFactory on your own servers. The infrastructure footprint is intentionally small - no Java runtime, no application server, no container orchestration required. A single modest Linux VPS covers all but the most demanding workloads.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Component</th>
      <th style="text-align: left; padding: 6px 10px;">Minimum</th>
      <th style="text-align: left; padding: 6px 10px;">Recommended</th>
      <th style="text-align: left; padding: 6px 10px;">Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">VeloxFactory server (Linux)</td>
      <td style="padding: 6px 10px;">1 vCPU, 1 GB RAM</td>
      <td style="padding: 6px 10px;">2 vCPU, 4 GB RAM</td>
      <td style="padding: 6px 10px;">A <a href="https://www.hetzner.com/cloud/" target="_blank">Hetzner CX22</a> (2 vCPU, 4 GB RAM, 40 GB NVMe) is more than sufficient for most deployments. Any comparable entry-level VPS or on-premises Linux server works equally well.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Disk</td>
      <td style="padding: 6px 10px;">10 GB</td>
      <td style="padding: 6px 10px;">40 GB</td>
      <td style="padding: 6px 10px;">Application and report thumbnails. Scale with report volume and history retention period.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Background Printing Service (Windows)</td>
      <td colspan="2" style="padding: 6px 10px;">Any Windows 10/11 machine or Windows Server with the target printers installed</td>
      <td style="padding: 6px 10px;">Runs as a lightweight console process - a few dozen MB of memory, negligible CPU. In most deployments, an existing Windows PC on the shop floor or in the office serves as the print host. No dedicated hardware required.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>MySQL and VeloxFactory can share the same VM</strong> for small to medium deployments. A dedicated database server is only worth considering when multiple VeloxFactory instances share one database, or under very high render throughput.
</div>

<h4 id="managed-hosting" style="color: #203671; margin-top: 1.4em;">Managed Hosting</h4>

If you prefer not to manage infrastructure yourself, VeloxFactory can be hosted and operated for you. Instances run exclusively in Germany on Hetzner hardware - reliable, fast, and GDPR-compliant by location. Updates, monitoring, and backups are handled on your behalf.

Managed hosting works well for most use cases. Depending on your network connection to the hosting location, render requests may see slightly higher latency compared to a locally deployed instance, typically not noticeable, but worth considering for high-frequency, latency-sensitive workloads such as real-time label printing on the shop floor.

For managed hosting enquiries, get in touch directly.

</div>

# Configuration and data models

<div style="text-align: justify;">

VeloxFactory is built around a small set of interconnected models. Understanding them is the key to understanding everything else, from how reports are set up, to how renderings are stored, to how print jobs and mails are dispatched, to how recurring work is scheduled.

---

<h3 id="naming-conventions" style="color: #203671; margin-top: 2.2em;">Field Naming Conventions</h3>

VeloxFactory uses two naming styles consistently throughout the system. Database columns and Laravel model attributes are always `snake_case`, for example `broadcast_id`, `report_config_id`, `created_by_token_id`. The API and frontend use `camelCase` for all request and response fields, the same fields become `broadcastId`, `reportConfigId`, `createdByTokenId`.

This split is consistent without exception: whenever you are working with the API or the frontend, use camelCase. Whenever you are looking at raw database records, migration files, or server-side model attributes, expect snake_case. Throughout this documentation, all field names and JSON examples follow the API convention: camelCase.

---

<h3 style="color: #203671; margin-top: 2.2em;">The Model Hierarchy</h3>

Every piece of data in VeloxFactory fits into a clear hierarchy. At the top sits the **ReportConfig**, the central entity. Everything else either belongs to it, describes it, or records what happened when it was used.

```
ReportContext                             ← Organisational label for grouping reports
ReportConnectionConfig                    ← Optional live database connection
ReportConfig                              ← The report template + all its metadata
  ├── ReportParameter                     ← Input values passed at render time
  ├── ReportField                         ← Output columns from the SQL query or data payload
  └── ReportResource                      ← Graphic file asset (image, logo)
        └── (links to) CommonReportResource   ← Shared asset, used by reports and mails
ReportHistoryRecord                       ← Record of a past rendering (optional)
  ├── ReportPrintTask                     ← A print job dispatched from a history record
  │     └── (uses) Printer                ← Master data of the target printer
  └── ReportMailTask                      ← A mail dispatched from a history record
        ├── (uses) Mailer                 ← SMTP account the mail is sent through
        ├── (uses) MailTemplate           ← Subject and body of the mail
        └── (embeds) CommonReportResource ← Graphics placed in the mail body

ScheduledJob                              ← A scheduled job: a render request, or the cleanup
  ├── (renders) ReportConfig              ← Render jobs only
  ├── (runs as) User + PersonalAccessToken  ← Owner and token a render job authenticates with
  ├── (notifies via) Mailer               ← Mailer the failure mail is sent through
  └── ScheduledJobRun                     ← Record of one execution, with trace id and result
```

Three of these are **master data**: `Printer`, `Mailer` and `MailTemplate` are maintained once under *Configuration* and then picked by name whenever a rendering is printed or mailed. They exist independently of any report.

---

<h3 style="color: #203671; margin-top: 2.2em;">ReportContext</h3>

A context is a **visual label** you assign to report configurations to group and identify them at a glance. It carries no functional logic, it is purely organisational.

| Field | Description |
|---|---|
| `context_name` | Display name of the context |
| `context_description` | Short description |
| `context_text_color` | Hex color for the label text |
| `context_badge_color` | Hex color for the badge background |
| `context_border_color` | Hex color for the badge border |

Every `ReportConfig` requires a context. A single context can be shared across any number of report configurations.

---

<h3 style="color: #203671; margin-top: 2.2em;">ReportConnectionConfig</h3>

A connection config represents a **live database connection** that VeloxFactory can use as a data source when rendering a report. When assigned to a `ReportConfig`, VeloxFactory executes the report's SQL query against this connection at render time and feeds the result rows into the report as field data.

| Field | Description |
|---|---|
| `connection_name` | Friendly name for this connection |
| `connection_driver` | Database driver (see table below) |
| `connection_host` | IP address of the database server |
| `connection_port` | Port (required) |
| `connection_database` | Database / schema name |
| `connection_username` | Username (stored encrypted) |
| `connection_password` | Password (stored encrypted) |
| `connection_test_query` | SQL query used to verify the connection |
| `connection_tested` | Whether the connection has been successfully tested |

**Supported drivers:**

| Driver | Database |
|---|---|
| `mysql` | MySQL |
| `mariadb` | MariaDB |
| `pgsql` | PostgreSQL |
| `sqlsrv` | Microsoft SQL Server |

**Connection status** is derived from `connection_tested`:

| Status | Meaning |
|---|---|
| <span style="color: #349b31; font-weight: 600;">approved</span> | Connection has been tested successfully |
| <span style="color: #525E5A;">unapproved</span> | Never tested or last test failed |

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>A report can only be rendered with a live connection if its status is <em>approved</em>.</strong> VeloxFactory will reject render requests for reports whose connection has not been successfully tested.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Network Requirements</h4>

VeloxFactory establishes the database connection directly from the server it runs on. The target database must therefore be **reachable from that host**, ideally within the same network or at minimum via a secured private channel.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Do not expose your database to the public internet.</strong> Configuring a publicly accessible database (or opening firewall ports to make one reachable) is a significant security risk and is strongly discouraged. If VeloxFactory and your database run in separate networks, use an encrypted VPN tunnel instead: <strong>WireGuard</strong> or <strong>OpenVPN</strong> are both well-suited for this purpose.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">When is a ReportConnectionConfig needed?</h4>

A `ReportConnectionConfig` is **optional** per `ReportConfig`. Whether you need one depends on how your report gets its data:

| Scenario | Connection needed? |
|---|---|
| Report has no detail band (purely static layout) | No |
| Report has a detail band, data delivered via API at render time | No |
| Report has a detail band and fetches data via SQL | **Yes** |

---

<h3 style="color: #203671; margin-top: 2.2em;">ReportConfig</h3>

The `ReportConfig` is the **core entity** of VeloxFactory. It represents a single JasperReports template (the `.jrxml` file) together with all the metadata VeloxFactory maintains about it.

| Field | Description |
|---|---|
| `report_name` | Display name of the report |
| `report_description` | Optional description |
| `report_file_name` | Internal filename of the stored `.jrxml` |
| `report_width` | Page width in mm (extracted from the `.jrxml` on upload) |
| `report_height` | Page height in mm (extracted from the `.jrxml` on upload) |
| `report_query` | SQL query, defined in VeloxFactory and stored in the database |
| `report_has_detail_band` | Whether the template contains a detail band (extracted on upload) |
| `report_context_id` | FK → `ReportContext` |
| `report_connection_config_id` | FK → `ReportConnectionConfig` (nullable) |
| `report_preview_base64` | Base64-encoded preview image |
| `report_thumbnail_base64` | Base64-encoded thumbnail image |

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The SQL query is not defined in the <code>.jrxml</code> file.</strong> It is written and managed directly in VeloxFactory and stored in the database as part of the <code>ReportConfig</code>. The <code>.jrxml</code> only defines which fields the query result maps to.
</div>

When a `.jrxml` file is uploaded, VeloxFactory **automatically analyses it** and creates the associated `ReportParameter`, `ReportField`, and `ReportResource` records. You then review and complete the auto-generated data, for example setting example values or uploading resource files.

<h4 style="color: #203671; margin-top: 1.4em;">ReportParameter</h4>

Parameters are the **inputs** passed into a report at render time: dates, IDs, filter values, flags, and so on.

| Field | Description |
|---|---|
| `parameter_name` | Parameter name as defined in the `.jrxml` |
| `parameter_data_type` | Java class name (e.g. `java.lang.String`, `java.lang.Integer`) |
| `parameter_required` | Read from the `required` custom property in the `.jrxml` |
| `parameter_evaluation` | Evaluation time, extracted from the `.jrxml` |
| `parameter_example_value` | Read from the `exampleValue` custom property in the `.jrxml` |

Both `parameter_required` and `parameter_example_value` are sourced from **custom properties** embedded in the `.jrxml` parameter definition. They can also be set manually in VeloxFactory after upload.

Three parameter name prefixes carry meaning beyond the render itself:

| Prefix | Meaning |
|---|---|
| `P_RESOURCE_` | The parameter holds a graphic file asset, VeloxFactory creates a `ReportResource` for it |
| `P_SCAN_` | The parameter is filled by a barcode scan in Scan2Print |
| `P_STATIC_` | The parameter is preset once per Scan2Print session and stays untouched between scans |

`P_SCAN_` and `P_STATIC_` are what makes a report usable in Scan2Print. Everything else about the report stays the same, the prefixes are a convention, not a separate model.

<h4 style="color: #203671; margin-top: 1.4em;">ReportField</h4>

Fields represent the **data columns** that populate the report's detail band, either from an SQL query result or from a data array delivered at render time.

| Field | Description |
|---|---|
| `field_name` | Field name as defined in the `.jrxml` |
| `field_data_type` | Java class name (e.g. `java.lang.String`, `java.math.BigDecimal`) |
| `field_example_value` | Read from the `exampleValue` custom property in the `.jrxml` |

`field_example_value` is used when rendering a preview without a live database connection.

<h4 style="color: #203671; margin-top: 1.4em;">ReportResource</h4>

Resources are **graphic file assets** (images and logos) embedded in the report template. They are referenced in the `.jrxml` via parameters following the `P_RESOURCE_` naming convention.

| Field | Description |
|---|---|
| `parameter_name` | The `P_RESOURCE_` parameter name as referenced in the `.jrxml` |
| `resource_file_name` | Filename of the directly uploaded file (nullable) |
| `common_report_resource_id` | FK → `CommonReportResource` (nullable) |

A `ReportResource` either holds its own uploaded file **or** it is linked to a `CommonReportResource`, never both at the same time. When linking to a common resource, the resource's own file is deleted and the common file is used in its place.

---

<h3 style="color: #203671; margin-top: 2.2em;">CommonReportResource</h3>

A `CommonReportResource` is a **shared graphic asset** (a company logo, a standard header image) that multiple report configurations can reference. Instead of uploading the same file to each report individually, you upload it once and link individual `ReportResource` records to it.

| Field | Description |
|---|---|
| `resource_name` | Display name, **unique**, and at the same time the placeholder token used in a mail body |
| `resource_description` | Optional description |
| `resource_file_name` | Internal filename of the stored file |
| `resource_mime_type` | Detected mime type, filled on upload |
| `resource_width` / `resource_height` | Pixel dimensions, read from the file |
| `resource_display_width` | Optional width the graphic is rendered with in a mail |
| `resource_alt_text` | Alternative text for the mail graphic |

A common resource serves two purposes. In a report it is the file behind a `P_RESOURCE_` parameter. In a mail template it is an **inline graphic**: the body references it by `[image.<resource_name>]`, and at send time the file is embedded into the mail itself, not linked from a server. A resource is mail capable when it is a real PNG or JPEG with readable dimensions, which is why the name has to be unique.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Linking is a one-way action.</strong> When a <code>ReportResource</code> is linked to a <code>CommonReportResource</code>, its own file is permanently deleted. Unlinking removes the reference but does not restore the file, you will need to re-upload it.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">ReportHistoryRecord</h3>

A `ReportHistoryRecord` captures the **full context of a rendering**: what was requested, what was returned, and whether it succeeded. Creating a history record is **optional** and controlled by the `createHistoryRecord` flag in the render request.

| Field | Description |
|---|---|
| `report_config_id` | FK → `ReportConfig` |
| `trace_id` | Unique identifier for this rendering run |
| `output_type` | How the PDF was returned (see below) |
| `report_api_payload` | The exact request payload sent to the render call |
| `report_api_response` | The full API response, stored for traceability |
| `report_pdf_base64` | Base64-encoded PDF content |
| `report_pdf_file_name` | Filename of the PDF on disk |
| `report_excel_file_name` | Filename of the xlsx export on disk, written when a mailing asked for one (nullable) |
| `report_thumbnail_base64` | Base64-encoded thumbnail of the first page (generated asynchronously) |
| `status` | Outcome of the rendering (see below) |

**Output types** (`output_type`):

| Value | Description |
|---|---|
| `base64` | PDF returned inline as a Base64 string |
| `url` | PDF stored as a file, a URL is returned |
| `preview` | Rendered for preview; file is not persisted |
| `none` | No PDF output (used for print-only flows) |

**Status values** (`status`):

| Value | Description |
|---|---|
| <span style="color: #349b31; font-weight: 600;">ok</span> | Rendering succeeded, PDF received |
| <span style="color: #525E5A;">render_fail</span> | No errors reported, but no PDF received |
| <span style="color: #c0392b;">error</span> | JasperReports returned one or more errors |
| <span style="color: #525E5A;">unknown</span> | Status cannot be determined |

History records are retained for a configurable number of days, set as a retention on the Purge Job. Thumbnails are generated asynchronously after rendering completes. Deleting a history record removes its PDF and its xlsx from disk together with the record.

---

<h3 style="color: #203671; margin-top: 2.2em;">ReportPrintTask</h3>

A `ReportPrintTask` represents a **print job** dispatched to a physical printer. It is always linked to a `ReportHistoryRecord`, you always print a specific past rendering, not a report config directly.

| Field | Description |
|---|---|
| `report_config_id` | FK → `ReportConfig` |
| `report_history_record_id` | FK → `ReportHistoryRecord` |
| `printer_id` | FK → `Printer` (nullable, set when the printer was picked from the master data) |
| `trace_id` | Unique identifier for this print run |
| `broadcast_id` | WebSocket channel ID for real-time status updates (nullable) |
| `printer_name` | Target printer name |
| `copies` | Number of copies to print |
| `output_file_name` | Filename of the PDF sent to the printer |
| `output_base64_string` | Base64-encoded PDF (consumed by the print service) |
| `error_message` | Error detail if printing failed |
| `status` | Current print status (see below) |

**Status values** (`status`):

| Value | Description |
|---|---|
| <span style="color: #203671; font-weight: 600;">pending</span> | Created, waiting for the print service |
| <span style="color: #349b31; font-weight: 600;">printed</span> | Successfully printed and confirmed |
| <span style="color: #c0392b;">error</span> | Printing failed |
| <span style="color: #525E5A;">unknown</span> | Status cannot be determined |

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Real-time updates via WebSocket</strong> only apply when <code>broadcastId</code> is provided in the render request. The C# print service subscribes to that channel, picks up the task, executes the print job, and reports status back. Without a <code>broadcastId</code>, the task is created silently, the print service must poll for new tasks.
</div>

---

<h3 id="printer" style="color: #203671; margin-top: 2.2em;">Printer</h3>

A `Printer` is **master data for a physical printer**. Without it, every print request had to carry the exact queue name of the print server, and everyone had to know it by heart. With it, a printer is configured once and then picked from a list.

| Field | Description |
|---|---|
| `printer_display_name` | The name people see, for example `Warehouse Label 01`. Unique, 3 to 50 characters |
| `printer_name` | The queue name on the print server, for example `WH-LABEL-01`. This is what the print service receives. Unique |
| `printer_type` | `label-printer`, `a4-printer`, `mfc-printer` or `digital-printer`. Descriptive, it drives icon and filter |
| `printer_location` | Where the machine stands, for example `Hall 2, Shipping` (nullable) |
| `printer_broadcast_id` | WebSocket channel of the print service instance that serves this printer (nullable) |
| `printer_description` | Free note, for example the loaded media (nullable) |
| `printer_is_active` | Inactive printers keep working in existing configurations but are no longer offered in pickers |

A render request may name either the display name or the queue name, VeloxFactory resolves both. A name that matches no master data record is still accepted and passed through unchanged, so integrations written before the master data existed keep working.

---

<h3 id="mailer" style="color: #203671; margin-top: 2.2em;">Mailer</h3>

A `Mailer` is **master data for one SMTP account**. It is to mailing what a `ReportConnectionConfig` is to data: the credentials live in one place, are tested from the UI, and are then referenced by name.

| Field | Description |
|---|---|
| `mailer_name` | Unique name used to reference the mailer in a request |
| `mailer_transport` | Transport, `smtp` by default |
| `mailer_host` / `mailer_port` | SMTP server and port |
| `mailer_scheme` | `smtp`, `smtps` or empty for the transport default |
| `mailer_username` / `mailer_password` | Credentials, stored encrypted |
| `mailer_from_address` / `mailer_from_name` | Sender of every mail sent through this mailer |
| `mailer_reply_to` | Optional reply-to address |
| `mailer_timeout` / `mailer_local_domain` | Optional transport details |
| `mailer_rate_limit_per_minute` | Max mails per minute, empty means the window is not enforced |
| `mailer_rate_limit_per_hour` | Max mails per hour, `500` by default |
| `mailer_rate_limit_per_day` | Max mails per day, empty means the window is not enforced |
| `mailer_is_active` | Inactive mailers are not offered and are refused by the API |
| `mailer_tested` | Whether a test mail has been sent successfully |
| `mailer_description` | Free note |

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The three rate limit windows are independent and all have to allow a send.</strong> Providers cap differently: Office 365 counts per minute, most hosters per hour, Gmail per day. A blocked send is not lost, it is queued and goes out when the window opens.
</div>

The global `MAIL_*` variables of Laravel are **not** used by the mailing pipeline. Every mail goes through a `Mailer` record, registered as a runtime mail connection for the duration of the send and removed again afterwards, exactly the way a `ReportConnectionConfig` handles its database connection.

---

<h3 id="mailtemplate" style="color: #203671; margin-top: 2.2em;">MailTemplate</h3>

A `MailTemplate` holds the **subject and the body** of a mail, both written once and filled with placeholders at send time.

| Field | Description |
|---|---|
| `mail_template_name` | Unique name used to reference the template in a request |
| `mail_template_subject` | Subject line, placeholders allowed |
| `mail_template_body_html` | HTML body, written in a rich text editor, placeholders allowed |
| `mail_template_header_color` | Optional override of the header bar colour |
| `mail_template_body_color` | Optional override of the body background |
| `mail_template_hide_logo` | Hides the logo in the header |
| `mail_template_logo_resource_id` | FK → `CommonReportResource`, an own logo for this template (nullable) |
| `mail_template_footer_text` | Optional override of the global footer text, placeholders allowed |
| `mail_template_is_active` | Inactive templates are not offered and are refused by the API |
| `mail_template_description` | Free note |

Placeholders use the `[token]` syntax and cover the render (`[traceId]`, `[reportFileName]`), the report, the history record, the user, the API token, the printer, every parameter (`[parameters.<NAME>]`), the data rows (`[data.first.<FIELD>]`), the current time (`[now.date]`), the recipients and the graphics (`[image.<RESOURCE_NAME>]`). The full catalog is available in the editor. An unknown token resolves to an empty string and is reported, it never fails a mail.

The four whitelabel fields are each a nullable override of a global default. Left empty, the mail uses the theme colours read from the application's own stylesheet, so a mail looks like the application without anything being configured twice.

---

<h3 id="reportmailtask" style="color: #203671; margin-top: 2.2em;">ReportMailTask</h3>

A `ReportMailTask` represents **one mail**. Like a `ReportPrintTask` it belongs to a `ReportHistoryRecord`, you always mail a specific past rendering.

| Field | Description |
|---|---|
| `report_history_record_id` | FK → `ReportHistoryRecord` (nullable if no history record was created) |
| `mailer_id` | FK → `Mailer` |
| `mail_template_id` | FK → `MailTemplate` |
| `trace_id` | Unique identifier for this mail run |
| `recipients_to` / `recipients_cc` / `recipients_bcc` | Resolved address lists |
| `mail_subject` | The **rendered** subject, placeholders already resolved |
| `mail_body_html` | The **rendered** body, stored so the UI can show what was actually sent |
| `attachment_pdf_file_name` | The PDF on the history disk, always `<trace id>.pdf` |
| `attachment_excel_file_name` | The xlsx on the history disk, always `<trace id>.xlsx` |
| `attachment_pdf_name` | The name the PDF carries as an attachment of the mail |
| `attachment_excel_name` | The name the xlsx carries as an attachment of the mail |
| `send_async` | Whether the mail was queued instead of sent synchronously |
| `dispatch_after` | Set when the rate limit pushed the send into the future |
| `sent_at` | Timestamp of the successful send |
| `error_message` | Error detail if sending failed |
| `status` | Current mail status (see below) |

**Status values** (`status`):

| Value | Description |
|---|---|
| <span style="color: #203671; font-weight: 600;">pending</span> | Created, not sent yet, or waiting for a rate limit window |
| <span style="color: #349b31; font-weight: 600;">sent</span> | Handed over to the SMTP server successfully |
| <span style="color: #c0392b;">error</span> | Sending failed |
| <span style="color: #525E5A;">unknown</span> | Status cannot be determined |

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>A mail task never owns a file.</strong> The PDF and the xlsx belong to the history record, stay downloadable from it and are deleted with it. The two attachment name columns only say what the files are called inside the mail. Mail tasks have their own retention on the Purge Job, independent of the history records.
</div>

The address columns hold what was actually used: a recipient given as a placeholder is stored resolved, so the mail task shows the addresses the mail went to.

---

<h3 id="scheduledjob" style="color: #203671; margin-top: 2.2em;">ScheduledJob</h3>

A `ScheduledJob` is **one scheduled job**: either a render request, or the consolidated cleanup that keeps the database and the disk from growing without bound. Both live in the same table and differ in their type and their payload. Either kind fires on a crontab expression or once at a fixed date and time.

| Field | Description |
|---|---|
| `schedule_name` | Display name, **unique** |
| `schedule_description` | Optional description |
| `schedule_type` | `render` or `purge` |
| `schedule_cron` | Five-field crontab expression. Empty means the schedule never fires on its own and can only be run manually |
| `schedule_timezone` | Timezone the expression and the single date are evaluated in. Empty means the application timezone |
| `schedule_run_at` | Date and time of a single run. Set in place of a cron expression, never beside one |
| `schedule_deactivate_after_run` | Whether the schedule sets itself inactive once its single run is done |
| `schedule_is_active` | An inactive schedule is never dispatched |
| `report_config_id` | FK → `ReportConfig`, render jobs only |
| `schedule_payload` | JSON. Render jobs: the render request body. Purge jobs: the retention map |
| `owner_user_id` | FK → `User`, the user a render job runs as |
| `owner_token_id` | FK → `PersonalAccessToken`, the token a render job authenticates with |
| `error_mailer_id` | FK → `Mailer`, the mailer the failure mail goes through (nullable) |
| `error_recipients_to` / `error_recipients_cc` / `error_recipients_bcc` | Address lists of the failure mail, plain addresses without placeholders |
| `schedule_last_run_at` | When the schedule last fired |
| `schedule_next_run_at` | When it fires next. Indexed, this is what the minute tick selects on. Empty once a single run has happened |
| `schedule_last_status` | Status of the most recent run |

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The owner and the token are references, not copies.</strong> Both are foreign keys with a delete restriction, so a user or a token that a schedule runs with cannot be removed while the schedule exists. A revoked token or a deactivated owner does not delete anything, it parks the schedule and is recorded as a skipped run.
</div>

A purge job carries its retentions in `schedule_payload`, one key per step: `printTaskDays`, `mailTaskDays`, `historyDays`, `orphanedFileDays` and `runLogDays`. A key set to `null` switches its step off.

---

<h3 id="scheduledjobrun" style="color: #203671; margin-top: 2.2em;">ScheduledJobRun</h3>

A `ScheduledJobRun` is **the record of one execution**. It belongs to its schedule and is deleted with it.

| Field | Description |
|---|---|
| `scheduled_job_id` | FK → `ScheduledJob`, cascading delete |
| `trace_id` | Identifier of this run, the same id the rendering and its mail carry |
| `status` | Outcome of the run (see below) |
| `trigger` | `schedule` for a run the cron fired, `manual` for *Run now* |
| `started_at` / `finished_at` / `duration_ms` | Timing of the run |
| `run_as_user_id` | The user the run authenticated as, kept even after the schedule's owner changed |
| `report_history_record_id` | FK → `ReportHistoryRecord` the run produced (nullable) |
| `response_status` | HTTP status the internal render call answered with |
| `result_summary` | JSON summary: what a render returned, or how many records each purge step deleted |
| `error_message` | Error detail if the run failed |
| `error_mail_sent` | Whether a failure mail went out for this run |
| `error_mail_suppressed` | How many failure mails the throttle held back |

**Status values** (`status`):

| Value | Description |
|---|---|
| <span style="color: #203671; font-weight: 600;">running</span> | The run started and has not finished yet |
| <span style="color: #349b31; font-weight: 600;">success</span> | Everything the run was asked to do went through |
| <span style="color: #F9A825; font-weight: 600;">warning</span> | The main work succeeded, something attached to it did not, for example the mail |
| <span style="color: #c0392b;">error</span> | The run failed |
| <span style="color: #525E5A;">skipped</span> | The run was not executed, for example because the owner is inactive or the token revoked |

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The history record is referenced, never owned.</strong> <code>report_history_record_id</code> is nulled when the record is deleted, so the history retention stays free to clean up what a run produced. The run log itself is cleaned up by the Purge Job.
</div>

---

<h3 id="user" style="color: #203671; margin-top: 2.2em;">User</h3>

The account a person or an integration signs in with. Only the field that governs the first sign-in is listed here, the permissions themselves are described in <a href="/books/veloxfactory/page/meet-the-frontend">Meet the frontend</a>.

| Field | Description |
|---|---|
| `password_change_required` | Set when an account must change its password before it can use the instance. `setup:base` sets it on the administrator it creates |

---

<h3 style="color: #203671; margin-top: 2.2em;">Audit Trail</h3>

Every model in VeloxFactory tracks who created and last updated a record, and which API token was used. This information is available on all records via the `withAudit=true` query parameter in the API.

| Field | Description |
|---|---|
| `created_at` | Timestamp of creation |
| `created_by` | User ID of the creator |
| `created_by_token_id` | API token ID used (if created via API) |
| `updated_at` | Timestamp of last update |
| `updated_by` | User ID of the last updater |
| `updated_by_token_id` | API token ID used (if updated via API) |

The `creationMethod` and `updateMethod` fields in the API response (`"Frontend"` vs. `"API"`) are derived automatically, based on whether a token was present on the request.

<h4 style="color: #203671; margin-top: 1.4em;">Security Log</h4>

Alongside `jobs`, VeloxFactory writes a `security` channel to `storage/logs/security/`, kept for 90 days. It records sign-ins and failed attempts, sign-outs, password changes, permission changes and the creation and revocation of API tokens.

---

<h3 id="environment-configuration" style="color: #203671; margin-top: 2.2em;">Environment Configuration</h3>

VeloxFactory's runtime behaviour is controlled via environment variables in `.env` or as container environment variables.

<h4 style="color: #203671; margin-top: 1.4em;">Application</h4>

| Variable | Default | Description |
|---|---|---|
| `APP_SCHEME` | `http` | URL scheme for generated links (`http` or `https`) |
| `API_RATE_LIMIT_PER_MINUTE` | `10` | Max API requests per minute per token |
| `PAGINATION_DEFAULT_COUNT` | `25` | Rows per page in the lookups of the frontend |
| `API_PER_PAGE_DEFAULT` | `25` | Records per page of an API list endpoint when the request sends no `perPage` |
| `API_PER_PAGE_MAX` | `100` | Largest `perPage` an API list endpoint accepts, a larger value is answered with 422 |

<h4 style="color: #203671; margin-top: 1.4em;">Security</h4>

| Variable | Default | Description |
|---|---|---|
| `SANCTUM_EXPIRATION` | `525600` | Lifetime of an API token in minutes, one year by default. Applies to tokens that already exist |
| `DB_TRUST_SERVER_CERTIFICATE` | `false` | Accept a self-signed certificate on a report database. Only for closed networks |
| `DB_ENCRYPT` | `true` | Encrypted connection to the report database |
| `OUTBOUND_HOST_ALLOWLIST` | empty | Comma separated hosts that Report Connection Configs and Mailers may point at. Empty means everything except loopback and link-local |
| `LOG_VIEWER_ROUTE` | `admin/log-viewer` | URL path of the Log Viewer |

<h4 style="color: #203671; margin-top: 1.4em;">Queue & Redis</h4>

| Variable | Default | Description |
|---|---|---|
| `QUEUE_CONNECTION` | `database` | Queue driver (must be set to `redis` for Horizon) |
| `REDIS_CLIENT` | `phpredis` | Redis client library (`phpredis` required) |
| `REDIS_HOST` | `127.0.0.1` | Redis server hostname or IP |
| `REDIS_PORT` | `6379` | Redis server port |
| `REDIS_PASSWORD` | `null` | Redis password (leave `null` if not set) |
| `REDIS_QUEUE_RETRY_AFTER` | `360` | Seconds before a reserved job on the `redis` connection is handed to another worker. Must stay above the longest worker timeout |
| `REDIS_SCHEDULER_QUEUE_RETRY_AFTER` | `1860` | The same for the `redis-scheduler` connection, which carries the long-running scheduled jobs |

<h4 style="color: #203671; margin-top: 1.4em;">Horizon</h4>

| Variable | Default | Description |
|---|---|---|
| `HORIZON_PATH` | `horizon` | URL path for the Horizon dashboard |
| `HORIZON_PREFIX` | derived from `APP_NAME` | Redis key prefix for all Horizon data |
| `HORIZON_DOMAIN` | - | Optional custom domain for the Horizon dashboard |

<h4 style="color: #203671; margin-top: 1.4em;">Job Scheduler</h4>

| Variable | Default | Description |
|---|---|---|
| `SCHEDULER_CATCHUP_GRACE_MINUTES` | `60` | A due run older than this is dropped instead of fired late, for example after the scheduler process was down |
| `SCHEDULER_RUN_LOCK_SECONDS` | `1800` | How long one run may hold its schedule's lock before another run may start |
| `SCHEDULER_ERROR_MAIL_THROTTLE_MINUTES` | `60` | At most one failure mail per schedule within this window |
| `SCHEDULER_PREVIEW_COUNT` | `5` | How many upcoming run dates the cron preview shows |
| `PURGE_SCHEDULER_RUNS_DAYS` | `90` | Retention of the run log, written into the Purge Job when it is created |

<h4 style="color: #203671; margin-top: 1.4em;">Mailing</h4>

| Variable | Default | Description |
|---|---|---|
| `MAIL_LOGO_RESOURCE` | - | Name of a mail capable `CommonReportResource`, embedded inline in the mail header |
| `MAIL_LOGO_URL` | - | Fallback used when `MAIL_LOGO_RESOURCE` is empty: an absolute, publicly reachable URL. Empty falls back to the application name as text |
| `MAIL_FOOTER_TEXT` | `Sent automatically by VeloxFactory.` | Footer of every mail, overridable per template |
| `MAIL_RATE_LIMIT_PER_MINUTE` | `0` | Default minute limit for a newly created mailer. `0` means not enforced. |
| `MAIL_RATE_LIMIT_PER_HOUR` | `500` | Default hour limit for a newly created mailer |
| `MAIL_RATE_LIMIT_PER_DAY` | `0` | Default day limit for a newly created mailer |
| `MAIL_THROTTLE_FALLBACK_TO_QUEUE` | `true` | A throttled synchronous send is queued instead of answered with an error |
| `MAIL_MAX_ASSET_SIZE_KB` | `2048` | Maximum size of a graphic that can be embedded into a mail |

The standard Laravel `MAIL_*` keys stay untouched. They are the framework fallback and are not used for report mails.

<h4 style="color: #203671; margin-top: 1.4em;">Retention & Purge</h4>

The retentions themselves live on the **Purge Job**, where they are edited in the frontend or through the API. These variables supply the values the Purge Job is created with, which makes them the right place to preconfigure an instance before it is set up. Changing one afterwards has no effect on an existing schedule.

| Variable | Default | Description |
|---|---|---|
| `PURGE_HISTORY_DAYS` | `30` | Age in days after which history records are deleted. `-1` creates the step switched off. |
| `PURGE_PRINTTASKS_DAYS` | `30` | Age in days after which print tasks are deleted. `-1` creates the step switched off. |
| `PURGE_MAILTASKS_DAYS` | `30` | Age in days after which mail tasks are deleted. `-1` creates the step switched off. |
| `PURGE_ORPHANED_FILES_DAYS` | `30` | Age in days after which orphaned files on disk are deleted. `-1` creates the step switched off. |

</div>

# Meet the frontend

<div style="text-align: justify;">

VeloxFactory comes with a built-in web frontend that gives you full access to every feature without writing a single line of code. It is built with Laravel Livewire, a reactive framework that delivers a dynamic, app-like feel while keeping everything server-rendered. No separate JavaScript build, no SPA complexity.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.index.png</code><br>Report Config overview, the landing page after login, with context badges and thumbnails.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Navigation</h3>

After logging in you land directly on the **Report Configs** overview, the central workspace. Dedicated scan users without edit rights for report configurations land on **Scan2Print** instead. All other sections are reachable from the main navigation.

| Section | What you manage here |
|---|---|
| **Report Configs** | Your report templates, the heart of VeloxFactory |
| **Report History Records** | Every past rendering with status, payload, and PDF |
| **Report Print Tasks** | Print jobs dispatched to the print service |
| **Mail Queue** | Every mail that was sent or is waiting to go out, see [Report Mailing](/books/veloxfactory/page/report-mailing-sending-renderings-by-mail) |
| **Job Scheduler** | Recurring renders and the cleanup run, plus the log of every run, see [The Job Scheduler](/books/veloxfactory/page/the-job-scheduler-work-that-happens-without-you) |
| **Scan2Print** | Mobile scan station that turns every barcode scan into a print job, see [Scan2Print: Printing labels by scanning](/books/veloxfactory/page/scan2print-printing-labels-by-scanning) |
| **Report Contexts** | Organisational labels and visual tags for reports |
| **Report Connection Configs** | Live database connections for SQL-driven reports |
| **Common Report Resources** | Shared graphic assets, used by reports and as inline graphics in mails |
| **Printers** | Master data for your physical printers, see [Printers](/books/veloxfactory/page/printers-master-data-for-your-print-targets) |
| **Mailers** | Master data for your SMTP accounts, see [Mailers](/books/veloxfactory/page/mailers-master-data-for-your-smtp-accounts) |
| **Mail Templates** | Subject, body and whitelabeling of your mails, see [Mail Templates](/books/veloxfactory/page/mail-templates-subject-body-and-whitelabeling) |
| **Users** | User accounts and API token management |

The last seven entries are grouped under **Configuration** in the main navigation. They are master data you set up once and then pick by name everywhere else.

---

<h3 style="color: #203671; margin-top: 2.2em;">The Lookup Pattern</h3>

Every section opens with a **list view**, powered by a Livewire Lookup component. These views work the same way across all sections, so once you know one, you know them all.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.index.filtered.png</code><br>Report Config overview with the filter bar open and active filter badges.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Full-Text Search</h4>

A search bar at the top filters records instantly as you type, no page reload, no submit button. Depending on the section, the search covers names, descriptions, file names, and query text.

<h4 style="color: #203671; margin-top: 1.4em;">Column Filters</h4>

Each list view offers contextual filter dropdowns tailored to the entity. For Report Configs, for example, you can filter by Context, Data Adapter, Creator, or date ranges for creation and last update. Active filters are indicated by a badge count on the respective dropdown so you always see at a glance which filters are in play.

<h4 style="color: #203671; margin-top: 1.4em;">Pagination</h4>

Results are paginated. The default page size is controlled by the `PAGINATION_DEFAULT_COUNT` environment variable (default: 25). See [Configuration and Data Models](/books/veloxfactory/page/configuration-and-data-models) for all available environment settings.

<h4 style="color: #203671; margin-top: 1.4em;">Persistent Filters and Paging</h4>

Every list view remembers its state. The search term, all column filters (checkboxes, dropdown selections, date ranges) and the current page are stored in your session as soon as you change them. You can open a record, switch to another section and come back later, the list shows exactly the same filters and the same page as before.

The state is kept separately for each list view. Filters set on History Records do not affect Report Configs or any other section.

To start over, click the **reset button** of the respective list view, the dark button with the curved arrow icon between the search bar and the green **Filter** button. It clears the search term and all filters of this view and returns to the first page. All other views keep their filters.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Filters are stored per user session.</strong> They survive page reloads and navigation, but are cleared on logout or when the session expires. Filters are never shared with other users or other browser sessions.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Working with Report Configs</h3>

The Report Config section has the richest set of actions and is where most of your day-to-day work happens.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.edit.png</code><br>Report Config edit view with parameters, fields and resources.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Uploading a Template</h4>

Report templates are designed in **Jaspersoft Studio** (compatible version: **6.21.5**) and exported as `.jrxml` files. Once you have a `.jrxml` ready, you upload it to VeloxFactory to create a new Report Config. VeloxFactory immediately analyses the file and automatically creates all associated Parameters, Fields, and Resources, based on what is defined in the template. You do not need to add these manually.

After upload you review the auto-generated records: set example values where missing, assign a Data Adapter if the report uses SQL, and upload any resource files that were detected.

<h4 style="color: #203671; margin-top: 1.4em;">Managing Parameters, Fields, and Resources</h4>

Parameters, Fields, and Resources each have their own section within the Report Config edit view. You can edit example values, mark parameters as required, upload resource files, or link a resource to a Common Report Resource, all from the same screen.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-resource.edit.png</code><br>Report Resource edit view with the file upload and the link to a Common Report Resource.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Generating Previews</h4>

Once all example values are set and all resource files are uploaded, you can generate a preview rendering directly from the edit view. VeloxFactory renders the report using the stored example values and stores the result as a preview image and thumbnail on the Report Config.

<h4 style="color: #203671; margin-top: 1.4em;">Rendering from the Frontend</h4>

You can trigger a full render directly from the frontend, without touching the API. A render dialog lets you fill in parameter values, choose an output type, and optionally dispatch a print job or a mail in the same step. The result is shown inline and logged as a History Record if desired.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>generate-pdf.create.png</code><br>Generate PDF page with the parameter inputs and the output settings.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Report Connection Configs</h3>

When creating or editing a Connection Config, the form includes a **Test Connection** button. Use it before saving, a connection must be in status <span style="color: #349b31; font-weight: 600;">approved</span> before VeloxFactory will use it for rendering. An untested or failed connection will cause render requests to be rejected.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-connection-config.edit.png</code><br>Report Connection Config edit form with the Test Connection button.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Report History Records</h3>

The History Records section gives you a full log of every rendering that was saved. For each record you can see the status, the exact parameters and data that were submitted, the raw JasperReports response, and, if the rendering succeeded, the resulting PDF.

From a History Record you can:

- **Download** the PDF directly to your browser, and the xlsx export if the record has one
- **Trigger a print job**, creates a new Print Task for this specific rendering and dispatches it to the print service
- **Send it by mail**, opens a dialog for mailer, template, recipients and attachments and creates a new Mail Task

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-history-record.show.png</code><br>Report History Record detail page with status, payload, PDF preview and the actions.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Downloads follow the read permission.</strong> Downloading a rendered PDF, an xlsx export, a report template or a resource file requires the read permission for that resource. Being signed in is not enough.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Report Print Tasks</h3>

The Print Tasks section shows all dispatched print jobs and their current status. For each task you can inspect the target printer, number of copies, and any error messages if printing failed.

If a task ended up in <span style="color: #c0392b;">error</span> state, you can **reset** it, the task returns to <span style="color: #203671; font-weight: 600;">pending</span> and the print service will pick it up again.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-print-task.index.png</code><br>Report Print Task overview with status badges, printer and the reset action.
</div>

---

<h3 id="mailing" style="color: #203671; margin-top: 2.2em;">Mailing</h3>

A rendered document can be mailed as well as printed. Three sections work together, and all three behave like every other list view described above.

**Mailers** hold your SMTP accounts: host, port, credentials, sender address and the dispatch rate limits. The form has a **Test Mailer** button and a **Send test mail** action, the same idea as Test Connection on a Connection Config. A mailer has to be active before it is offered anywhere.

**Mail Templates** hold subject and body. The body is written in a built-in rich text editor, so no HTML knowledge is needed. A button opens the **placeholder catalog**, a searchable table of every token the template can use, each one copyable with a click. A preview shows the finished mail exactly as a recipient will see it, including the colours and the logo, which can be overridden per template.

**Mail Queue** lists every mail task with recipients, subject, attachments, mode and status. Opening one shows the mail that was actually sent, rendered in place. From there a mail can be **repeated**: a dialog lets you correct the mailer or the recipients before it goes out again, so a mail sent to the wrong address is fixed rather than cloned.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mail-template.edit.png</code><br>Mail Template edit view with the rich text editor, the placeholder button and the whitelabel toggles.
</div>

Mailing itself is never configured here. It is requested per rendering, in the Generate PDF page, in the Scan2Print settings, in the mail dialog of a History Record, in a render job of the Job Scheduler, or in the `mailing` segment of a render request. The three sections above only provide the building blocks.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mailing.history-record-modal.png</code><br>The mail dialog of a Report History Record with mailer, template, recipients and attachments.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ Full detail is on the dedicated pages: <a href="/books/veloxfactory/page/report-mailing-sending-renderings-by-mail">Report Mailing</a>, <a href="/books/veloxfactory/page/mailers-master-data-for-your-smtp-accounts">Mailers</a> and <a href="/books/veloxfactory/page/mail-templates-subject-body-and-whitelabeling">Mail Templates</a>.
</div>

---

<h3 id="scheduling" style="color: #203671; margin-top: 2.2em;">Scheduling</h3>

Work that repeats has its own section. **Job Scheduler** lists every schedule with its type, its crontab expression, the next and the last run, and how that run ended. A **Run Log** button opens the full history of every run of every schedule, each with its trace id.

Creating a schedule starts with the type, a render job or the cleanup run, and the form follows from there. The expression is entered either in a guided mode with frequency, time and weekday pickers or as plain crontab syntax; both write the same field, and the next five run times are shown while you type. A render job additionally takes the report, an API token, the output and print settings, and the same parameter and data row tables as the Generate PDF page, here with a switch per field for entering a placeholder such as `[now.date]` instead of a fixed value.

**Run now** starts a schedule immediately without moving its next slot, which is how a new one is tried before it is switched on.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scheduler.overview.png</code><br>The Job Scheduler overview with type, cron expression, next run, last run and status per schedule.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ Full detail is on the dedicated pages: <a href="/books/veloxfactory/page/the-job-scheduler-work-that-happens-without-you">The Job Scheduler</a>, <a href="/books/veloxfactory/page/render-jobs-a-render-request-with-a-clock-attached">Render Jobs</a> and <a href="/books/veloxfactory/page/the-purge-job-one-schedule-for-all-cleanup">The Purge Job</a>.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Users and API Tokens</h3>

User accounts are managed under the **Users** section, accessible to administrators only. Each user can hold one or more named API tokens, which grant API access under that user's identity.

From the user edit view you can:

- Create a new token and copy it immediately after generation (it is only shown once)
- Revoke any existing token with immediate effect
- Reset another user's password, or change your own after confirming the current one

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>user.edit1.png</code><br>User edit view, account data and API token management.
</div>

---
<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>user.edit2.png</code><br>User edit view, the permission sections.
</div>

---
<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>create-token.modal.png</code><br>Create API token dialog, the token is shown once.
</div>


<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Copy your token immediately after creation.</strong> VeloxFactory only displays the token value once. If you lose it, you will need to revoke the old token and create a new one.
</div>

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Tokens are issued and revoked here, never through the API.</strong> A request that authenticates with an API token cannot create or revoke tokens and is answered with <code>403 API tokens can only be managed from the web interface</code>. That is deliberate: a token that could issue another one would survive its own revocation, and revoking a leaked token would not actually lock it out.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Permissions</h4>

Every user in VeloxFactory has a set of permissions that controls what they can see and do, both in the frontend and via the API. Since both use the same underlying controllers, **a permission that restricts an action in the frontend restricts the exact same action via API, and vice versa.** There is no way to grant API-only or frontend-only access to a resource.

Permissions fall into two categories: **global** and **per-resource**.

**Global permissions:**

| Permission | Effect |
|---|---|
| `global:admin` | Full access to everything, including user management. Only an administrator may grant or withdraw permissions, activate or deactivate accounts, and manage other users' API tokens |
| `global:use-api` | **Required for every API request.** Without it, each call answers `403 This user is not permitted to use the API`. Also allows managing one's own API tokens in the web interface |

**Per-resource permissions**, available for each of the following resources: `report-config`, `report-connection-config`, `report-context`, `report-history-record`, `report-print-task`, `report-mail-task`, `common-report-resource`, `printer`, `mailer`, `mail-template`, `scheduled-job`:

| Permission | Effect |
|---|---|
| `<resource>:full` | Full read/create/update/delete access to this resource |
| `<resource>:read` | View records only |
| `<resource>:create` | Create new records |
| `<resource>:update` | Edit existing records |
| `<resource>:delete` | Delete records |

`global:admin` always implies full access to all resources and overrides any per-resource setting.

`scheduled-job:*` also covers the run log. Changing or starting a render job is limited to its owner and to administrators, because it acts with the owner's permissions; reading it is not.

**Feature permissions:**

| Permission | Effect |
|---|---|
| `scan2print:use` | Access to [Scan2Print](/books/veloxfactory/page/scan2print-printing-labels-by-scanning). Implicitly includes `report-config:read`, `report-print-task:read` and `:create`, `report-history-record:create` and `:update` |
| `scheduled-job:create` | Creating a schedule means rendering, printing and mailing with it, so it implicitly includes `report-config:read`, `report-print-task:create`, `report-mail-task:create` and `report-history-record:create` and `:update`. `scheduled-job:full` does the same |

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Permissions are administrator territory.</strong> Users can edit their own name and e-mail address, but not their own permissions. The permission section is not shown to them, and a request that carries a permission list is answered with the user's current set instead. The same applies to activating and deactivating an account. VeloxFactory also refuses to let the last remaining administrator give up <code>global:admin</code> or be deactivated, so an instance can never end up without one.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>API token permissions are inherited from the user.</strong> A token does not have its own permission set, it acts with exactly the permissions of the user it belongs to, evaluated on every single request. Withdrawing a permission takes effect on the next call, there is nothing to re-issue. Deactivating a user revokes all of their tokens at once, and tokens expire after the period configured in <code>SANCTUM_EXPIRATION</code> (one year by default).
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Signing in</h3>

The login form answers every failed attempt the same way, with `Invalid credentials.` Whether the address belongs to an account, whether that account is deactivated and whether the password was wrong are deliberately indistinguishable, so the form cannot be used to find out which addresses exist on an instance.

Attempts are limited to five per minute, counted per source address and per e-mail address. Beyond that the form answers `429 Too Many Requests` until the minute is over. Successful and failed sign-ins, sign-outs, password changes, permission changes and token operations are written to a dedicated `security` log channel.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Changing your own password needs the current one.</strong> An administrator resetting someone else's password does not. Either way, every API token of that account is revoked afterwards, so a password change really does end all access that was granted before it.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Frontend vs. API - What is the Difference?</h3>

The short answer: there is no feature difference. The frontend calls the exact same controller methods as the API. Everything you can do in the UI, you can also automate via API.

The frontend is optimised for interactive, human-driven workflows, exploring reports, reviewing history records, testing connections. The API is the right choice when you want to integrate rendering into external systems, automate repetitive tasks, or process results programmatically.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ Ready to explore the API? See <a href="/books/veloxfactory/page/meet-the-api">Meet the API</a> for authentication, query parameters, and a practical render example.
</div>

</div>

# Meet the API

<div style="text-align: justify;">

VeloxFactory ships with a fully capable REST API, and it is not an afterthought. Every action you can perform in the frontend can also be performed via the API, because **the frontend and the API share the same controller methods**. There is no separate implementation, no feature gap, no second-class citizen.

This makes the API a genuine alternative to the UI, not just an integration bolt-on. Automate report rendering in your CI pipeline, trigger prints from your ERP, manage report configurations programmatically, have VeloxFactory run the whole thing on a schedule, all with the same logic that powers the frontend you already know.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ For the full endpoint reference including all parameters and response schemas, explore the live demo API documentation at <a href="https://demo.veloxfactory.dev" target="_blank">demo.veloxfactory.dev</a>.
<p style="margin: 6px 0 0 0;"><strong>Login:</strong> <code>demo@veloxfactory.dev</code> &nbsp;·&nbsp; <strong>Password:</strong> <code>demo</code></p>
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Authentication</h3>

The API uses **token-based authentication** via Laravel Sanctum. Every request must include a Bearer token in the `Authorization` header.

```http
Authorization: Bearer your-api-token
```

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Every API request requires <code>global:use-api</code>.</strong> The permission is checked on each call, not only when a token is issued. A token whose user does not hold it is answered with 403, even if the token itself is valid and the user has the resource permissions.
</div>

Tokens are issued and revoked per user in the user management section of the frontend. A request authenticated by a token cannot create or revoke tokens, so a leaked token cannot renew itself and revoking it actually locks it out. A revoked token is rejected immediately, there is no grace period.

```json
{
  "success": false,
  "errors": ["This API token has been revoked."],
  "status": 401
}
```

The audit trail records which token was used for every create and update operation across all models, so every API action is fully traceable.

<h4 style="color: #203671; margin-top: 1.4em;">Authentication Errors</h4>

| Status | Message | When |
|---|---|---|
| 401 | `This user account is disabled.` | The account behind the token was deactivated |
| 401 | `This API token has been revoked.` | The token was revoked, or every token of the user was revoked by a password change or a deactivation |
| 401 | Token expired | The token is older than `SANCTUM_EXPIRATION` |
| 403 | `This user is not permitted to use the API.` | The user is missing `global:use-api` |
| 403 | `API tokens can only be managed from the web interface.` | A token tried to create or revoke a token |

---

<h3 style="color: #203671; margin-top: 2.2em;">Base URL</h3>

All API v1 endpoints are prefixed with:

```
/api/v1/
```

---

<h3 id="referencing" style="color: #203671; margin-top: 2.2em;">Referencing Records</h3>

Wherever a request points at another record, it accepts either the numeric ID or the unique name of that record. These two are the same call:

```http
POST /api/v1/report-config/12/render
POST /api/v1/report-config/Shift Report/render
```

The same holds inside request bodies, for report contexts, connection configs, mailers, mail templates and schedules alike, so a payload stays readable without a lookup table of IDs. A Report History Record is addressed by its ID or by its trace id, which is what makes a trace id worth carrying through your own systems.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>A value of digits only is always read as an ID.</strong> That is why a name cannot consist of digits only. VeloxFactory refuses such a name when a record is created or renamed, so <code>4711</code> is never ambiguous.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Response Structure</h3>

All API responses follow a consistent envelope format.

**Success, list endpoint:**

```json
{
  "success": true,
  "count": 25,
  "data": [ { ... }, { ... } ],
  "links": {
    "first": "https://your-instance/api/v1/report-print-task?status=pending&page=1",
    "last": "https://your-instance/api/v1/report-print-task?status=pending&page=3",
    "prev": null,
    "next": "https://your-instance/api/v1/report-print-task?status=pending&page=2"
  },
  "meta": {
    "pagination": {
      "currentPage": 1,
      "lastPage": 3,
      "perPage": 25,
      "total": 61,
      "from": 1,
      "to": 25,
      "hasMorePages": true,
      "path": "https://your-instance/api/v1/report-print-task"
    }
  },
  "status": 200
}
```

**Success, single record:**

```json
{
  "success": true,
  "count": 1,
  "data": { ... },
  "links": { "first": null, "last": null, "prev": null, "next": null },
  "meta": {
    "pagination": {
      "currentPage": 1,
      "lastPage": 1,
      "perPage": 1,
      "total": 1,
      "from": 1,
      "to": 1,
      "hasMorePages": false,
      "path": "https://your-instance/api/v1/report-print-task/256"
    }
  },
  "status": 200
}
```

**Error:**

```json
{
  "success": false,
  "errors": ["Descriptive error message"],
  "meta": {},
  "status": 404
}
```

Every success response that carries `data` has the same shape, whether it returns a list, a single record, a freshly created record or a render result. A client parses one envelope for every endpoint.

- `count` is the number of records in `data`: the records on the current page for a list, `1` for a single record.
- `links` point to the first, last, previous and next page. A link that does not exist is `null`. Responses that are not paginated have all four links `null`.
- `meta.pagination` describes the page, see [Pagination](#pagination). A response that is not paginated reports exactly one page.
- `meta` carries further contextual information next to `pagination`, such as the `traceId` of a render run or the `mailing` result.

Error responses have no `links` and no `pagination` block.

---

<h3 id="pagination" style="color: #203671; margin-top: 2.2em;">Pagination</h3>

All list endpoints (`GET /user`, `GET /report-config`, `GET /report-print-task` and every other `GET` on a resource without an id) return their records page by page. Two query parameters control the page:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `page` | integer | `1` | Page to return, starting at `1`. |
| `perPage` | integer | `25` | Records per page, `1` to `100`. |

The values of the `pagination` block:

| Field | Meaning |
|---|---|
| `currentPage` | The page in this response. |
| `lastPage` | Number of the last page, at least `1`. |
| `perPage` | Page size used for this response. |
| `total` | Number of all records matching the request, across all pages. |
| `from` / `to` | Position of the first and last record of this page within `total`, `null` on an empty page. |
| `hasMorePages` | `true` as long as a next page exists. |
| `path` | URL of the endpoint without query string. |

Filters such as `status`, `type` or `isActive` apply before the records are split into pages, so `total` counts the filtered result. The links in `links` carry all query parameters of the request, a client follows `links.next` until it is `null` and keeps its filters on every page.

A page behind the last one returns `200` with an empty `data` array. A `page` below `1`, a `perPage` above `100` or a non-numeric value returns `422`, and so does a filter value the endpoint does not know, for example `status=foo` on `GET /report-print-task`.

To read every record, request `perPage=100` and follow `links.next`. Every page is a request of its own and counts against the [rate limit](#rate-limiting).

The two placeholder catalogs (`GET /mail-template/placeholders`, `GET /scheduled-job/placeholders`) are not paginated and always return one page.

The page size default and maximum are set with `API_PER_PAGE_DEFAULT` and `API_PER_PAGE_MAX`, see [Configuration and Data Models](/books/veloxfactory/page/configuration-and-data-models). A default above the maximum is reduced to the maximum.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong><code>limit</code> is a deprecated alias of <code>perPage</code></strong> and will be removed in a future version. <code>perPage</code> takes precedence when both are sent, and <code>limit=0</code> returns the maximum page size. There is no request that returns all records at once. A response to a request that uses <code>limit</code> without <code>perPage</code> carries the header <code>Deprecation: true</code> and a note in <code>meta.messages</code>.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Query Parameters</h3>

Most `GET` endpoints and the render endpoint share a common set of query parameters that control what is included in the response. They are additive, combine them freely.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `page`, `perPage` | integer | `1`, `25` | Page and page size on list endpoints, see [Pagination](#pagination). |
| `withRelations` | boolean | `false` | Include related models (e.g. parameters, fields, resources on a ReportConfig). |
| `recursiveRelations` | boolean | `false` | Include nested relations within relations. |
| `withMedia` | boolean | `false` | Include Base64-encoded file content, previews, and thumbnails. |
| `withAudit` | boolean | `false` | Include the audit segment on each record (timestamps, users, token, method). |

**Example**, retrieve a single report config with all relations, media, and audit data:

```http
GET /api/v1/report-config/1?withRelations=true&withMedia=true&withAudit=true
```

<h4 style="color: #203671; margin-top: 1.4em;">Audit Segment</h4>

When `withAudit=true` is set, every record in the response carries an `audit` block:

```json
"audit": {
  "createdAt": "2026-05-08 11:26:30",
  "createdByUser": 2,
  "createdByToken": null,
  "creationMethod": "Frontend",
  "updatedAt": "2026-05-08 13:31:35",
  "updatedByUser": 1,
  "updatedByToken": "Postman",
  "updateMethod": "API"
}
```

`creationMethod` and `updateMethod` are derived automatically, `"API"` when a token was used, `"Frontend"` when the action came through the UI. A run of a scheduled job fills these exactly like a direct call does, with the owner of the schedule and the token it runs with.

---

<h3 style="color: #203671; margin-top: 2.2em;">Render-Specific Request Body</h3>

The render endpoint (`POST /api/v1/report-config/{id}/render`) accepts the following fields in the **request body** (JSON).

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Field</th>
      <th style="text-align: left; padding: 6px 10px;">Type</th>
      <th style="text-align: left; padding: 6px 10px;">Required</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>outputType</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;">✅</td><td style="padding: 6px 10px;">How to return the PDF: <code>base64</code>, <code>url</code>, <code>preview</code>, or <code>none</code></td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>createHistoryRecord</code></td><td style="padding: 6px 10px;">boolean</td><td style="padding: 6px 10px;">✅</td><td style="padding: 6px 10px;">Whether to persist a <code>ReportHistoryRecord</code> for this render</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>createPrintTask</code></td><td style="padding: 6px 10px;">boolean</td><td style="padding: 6px 10px;">✅</td><td style="padding: 6px 10px;">Whether to create a <code>ReportPrintTask</code> after rendering</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>printerName</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;">if <code>createPrintTask</code></td><td style="padding: 6px 10px;">Name of the target printer</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>useExampleValues</code></td><td style="padding: 6px 10px;">boolean</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Use stored example values instead of supplying parameters manually</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>parameters</code></td><td style="padding: 6px 10px;">object</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Key-value map of parameter names to values</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>resourceOverrides</code></td><td style="padding: 6px 10px;">object</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Per-render override for <code>P_RESOURCE_*</code> image parameters, keyed by parameter name. A path has to point at a readable file inside the resources folder, a URL has to be <code>https://</code>, or the image is supplied as a Base64 upload. See <a href="/books/veloxfactory/page/rendering-with-our-powerful-api">Rendering with our powerful API</a> for the full reference.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>data</code></td><td style="padding: 6px 10px;">array</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Array of data rows, for reports without a live DB connection</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>numberOfCopies</code></td><td style="padding: 6px 10px;">integer</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Number of print copies (default: 1)</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>broadcastId</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">WebSocket channel ID, triggers real-time status updates for the print task</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>traceId</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Custom trace ID; auto-generated as UUID if omitted. Unique across all history records</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>laconicResponse</code></td><td style="padding: 6px 10px;">boolean</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Strip the response to just the PDF output (see below)</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>mailing</code></td><td style="padding: 6px 10px;">object</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Send the rendering by mail once it succeeded: mailer, mail template, recipients, attachments and sync or queued dispatch. See <a href="/books/veloxfactory/page/report-mailing-sending-renderings-by-mail">Report Mailing</a> for the full reference.</td></tr>
  </tbody>
</table>

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Output type <code>none</code> requires <code>createPrintTask: true</code>.</strong> Rendering without any output and without a printer to send it to is rejected, and a <code>mailing</code> segment does not take the place of the print task. <code>preview</code> and <code>mailing</code> are refused together as well, a preview render deletes its own file immediately and has nothing to attach.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Laconic Responses</h4>

By default, a render response is fully populated, it includes the echoed input, the PDF output, the linked `ReportConfig`, the created `ReportHistoryRecord`, and the `ReportPrintTask` and `ReportMailTask` if applicable. In high-frequency or bandwidth-sensitive scenarios, add `laconicResponse=true` to strip everything down to just the PDF output.

<h4 style="color: #203671; margin-top: 1.4em;">The Same Body on a Schedule</h4>

This body is also what a render job of the Job Scheduler stores. A request you have tested here can be dropped into the `payload` of a schedule unchanged, minus `traceId`, which every run generates for itself. See [Render Jobs](/books/veloxfactory/page/render-jobs-a-render-request-with-a-clock-attached).

---

<h3 id="rate-limiting" style="color: #203671; margin-top: 2.2em;">Rate Limiting</h3>

The API enforces a configurable rate limit per token. The default is **10 requests per minute**. When the limit is exceeded:

```json
{
  "success": false,
  "message": "Too many requests! The current rate limiting is 10 requests per minute.",
  "status": 429
}
```

The limit is adjusted via the `API_RATE_LIMIT_PER_MINUTE` environment variable, see [Configuration and Data Models](/books/veloxfactory/page/configuration-and-data-models).

---

<h3 style="color: #203671; margin-top: 2.2em;">Endpoints Overview</h3>

| Resource | Available operations |
|---|---|
| **User** | List, show, create, update, change password, enable/disable, create/revoke tokens (web interface only) |
| **ReportContext** | List, show, create, update, delete |
| **ReportConnectionConfig** | List, show, create, update, delete, test connection |
| **CommonReportResource** | List, show, create, update, delete |
| **ReportConfig** | List, show, create, update, delete, update resources / parameters / fields, generate previews, **render** |
| **ReportResource** | Link / unlink CommonReportResource |
| **ReportHistoryRecord** | List, show, create, update, delete, print, **mail** |
| **ReportPrintTask** | List, show, create, update, delete, set printed, set status |
| **ReportMailTask** | List, show, create, delete, repeat |
| **Printer** | List, show, create, update, delete |
| **Mailer** | List, show, create, update, delete, test configuration, send test mail |
| **MailTemplate** | List, show, create, update, delete, placeholder catalog, preview |
| **ScheduledJob** | List, show, create, update, delete, **run now**, cron preview, placeholder catalog |
| **ScheduledJobRun** | List, show, delete |

---

<h3 style="color: #203671; margin-top: 2.2em;">A Practical Example: The Full Render Response</h3>

A render call returns a `ReportRendering` object. By default it is fully populated, you see the input you sent, the PDF output, and the linked records that were created.

```json
{
  "success": true,
  "count": 1,
  "data": {
    "model": "ReportRendering",
    "traceId": "a3f9c1d2-4e87-4b2a-9f1c-d3e8b7a20f61",
    "input": {
      "parameters": {
        "P_ARTICLE_NUMBER": "4561287-154"
      },
      "data": null
    },
    "output": {
      "reportPdfFileName": "a3f9c1d2-4e87-4b2a-9f1c-d3e8b7a20f61.pdf",
      "reportPdfBase64": "JVBERi0xLjQ...",
      "reportUrl": null
    },
    "reportConfig": { "..." },
    "reportHistoryRecord": { "..." },
    "reportPrintTask": null
  },
  "links": { "first": null, "last": null, "prev": null, "next": null },
  "meta": {
    "traceId": "a3f9c1d2-4e87-4b2a-9f1c-d3e8b7a20f61",
    "pagination": {
      "currentPage": 1,
      "lastPage": 1,
      "perPage": 1,
      "total": 1,
      "from": 1,
      "to": 1,
      "hasMorePages": false,
      "path": "https://your-instance/api/v1/report-config/12/render"
    }
  },
  "status": 200
}
```

Add `laconicResponse=true` and the response collapses to just `output`, no echoed input, no embedded related records. Ideal for automated pipelines that only need the PDF.

</div>

# Try it out

<div style="text-align: justify;">

The demo instance gives you a fully functional VeloxFactory environment, pre-loaded with report templates and example data. Everything you can do in a production setup you can do here, with one exception: print tasks do not reach a real printer.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.index.png</code><br>Report Config overview, the landing page after login, with context badges and thumbnails.
</div>

---

<h3 id="access" style="color: #203671; margin-top: 2.2em;">Access</h3>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
<strong>Demo URL:</strong> <a href="https://demo.veloxfactory.dev" target="_blank">demo.veloxfactory.dev</a><br>
<strong>Login:</strong> <code>demo@veloxfactory.dev</code> &nbsp;·&nbsp; <strong>Password:</strong> <code>demo</code>
</div>

The demo account has full access to all report data, Report Configs, History Records, Print Tasks, Mail Tasks, Connection Configs, Contexts, Common Resources, Printers, Mailers and Mail Templates. It can also generate and use API tokens. User management is not available.

---

<h3 id="guided-tour" style="color: #203671; margin-top: 2.2em;">Guided Tour</h3>

Work through these steps in order for a complete first look at VeloxFactory.

<h4 style="color: #203671; margin-top: 1.4em;">Step 1 - Browse the Report Configs</h4>

After logging in you land on the **Report Configs** list. This is the central workspace. Each card shows a thumbnail preview of the report and its context badge.

Use the search bar to filter by name, or use the dropdown filters to narrow by context or data adapter. Try typing part of a report name, the list updates instantly, no page reload.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.index.filtered.png</code><br>Report Config overview with the filter bar open and active filter badges.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Step 2 - Explore a Report Config</h4>

Click any report to open its detail view. Here you can inspect:

- **Parameters** - the inputs the report expects at render time, with their data types and example values
- **Fields** - the data columns that populate the report body
- **Resources** - image assets embedded in the template (logos, icons)
- **Preview** - the rendered thumbnail, generated from the stored example values

Scroll through the sections to get a feel for what VeloxFactory extracts from a `.jrxml` file automatically on upload.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.edit.png</code><br>Report Config edit view with parameters, fields and resources.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Step 3 - Render a Report from the Frontend</h4>

From within a Report Config, click the **Generate PDF** button. A dialog opens with pre-filled parameter values (taken from the example values stored on the config). You can edit any of them before rendering.

Select an output type, **Base64** is fine for a quick look, and click **Generate PDF**. VeloxFactory processes the request and shows the resulting PDF inline within seconds. No API call needed, no setup required.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>generate-pdf.create.png</code><br>Generate PDF page with the parameter inputs and the output settings.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Step 4 - Check the History Record</h4>

Navigate to **Report History Records**. Your render from Step 3 appears at the top of the list (if you left `createHistoryRecord` enabled in the dialog). Open it to see the full log: the exact request payload, the API response, the rendered PDF, and a thumbnail.

From here you can download the PDF directly, trigger a reprint, which creates a new Print Task, or send the document by mail, which creates a new Mail Task.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-history-record.show.png</code><br>Report History Record detail page with status, payload, PDF preview and the actions.
</div>

---

<h3 id="try-yourself" style="color: #203671; margin-top: 2.2em;">Try It Yourself</h3>

Once you have completed the tour, these are good next things to explore on your own.

<h4 style="color: #203671; margin-top: 1.4em;">Upload Your Own Template</h4>

If you have a `.jrxml` file designed in Jaspersoft Studio, you can upload it directly to the demo. Go to **Report Configs → Create**, attach the file, and let VeloxFactory analyse it. Within seconds, all parameters, fields, and resources are detected and listed automatically.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Use Jaspersoft Studio 6.21.5.</strong> This is the version compatible with VeloxFactory's render engine. Download it from the <a href="https://community.jaspersoft.com/download-jaspersoft/community-edition/jaspersoft-studio_windows_6.x" target="_blank">Jaspersoft Community</a>. Reports created with a significantly newer version may use features the render engine does not support.
</div>

After upload, set any missing example values, upload resource files if needed, and click **Generate Preview** to produce a thumbnail. The report is then ready to render.

<h4 style="color: #203671; margin-top: 1.4em;">Call the API Directly</h4>

The demo account has API access enabled. To get a token, open the user menu in the top right corner and go to **Account Settings → API Tokens**, copy it immediately, it is only shown once.

With the token in hand, render any report with a single HTTP request:

```bash
curl -X POST \
  https://demo.veloxfactory.dev/api/v1/report-config/{reportName}/render \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "outputType": "base64",
    "useExampleValues": true,
    "createHistoryRecord": true
  }'
```

Replace `{reportName}` with the `report_name` of any config from the list (visible in the detail view). The `useExampleValues` flag tells VeloxFactory to use the stored example values instead of requiring you to pass parameters and data manually, ideal for a first test.

The response contains the rendered PDF as a Base64 string and a trace ID you can look up in History Records.

<h4 style="color: #203671; margin-top: 1.4em;">Send a Report by Mail</h4>

Mailing is the second way a rendering leaves VeloxFactory. Two mailers are waiting for you under *Configuration*.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Mailer</th>
      <th style="text-align: left; padding: 6px 10px;">What it is for</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Demo Log Mailer</strong></td>
      <td style="padding: 6px 10px;">Writes every mail into the application log instead of sending it. Ready to use, and the safe way to follow the whole path from render to mail task without anything leaving the machine.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Demo SMTP Mailer</strong></td>
      <td style="padding: 6px 10px;">The starting point for a real send. It carries placeholder values, <code>smtp.example.com</code> on port 587 with scheme <code>smtp</code>, user and sender <code>change-me@example.com</code>, and it is inactive so nobody picks it before it works.</td>
    </tr>
  </tbody>
</table>

To send for real, open the SMTP mailer, replace host, port, credentials and sender with the data of a mailbox you own, send a test mail from the form, and set it active. Then write a **Mail Template** with a subject and a body. The editor has a placeholder button that lists every token the template can use.

With a mailer and a template in place, open any History Record and click **Mail**, or switch on *Send results via Mail* in the Output Settings of the Generate PDF page. The mail goes out with the PDF attached, and optionally with an xlsx export of the same data. Every mail shows up in the **Mail Queue** with its status, its recipients and the body that was actually sent.

<h4 style="color: #203671; margin-top: 1.4em;">Explore Print Tasks</h4>

When rendering via the frontend or API, enable the print task option and provide any printer name (e.g. `demo-printer`). VeloxFactory creates a `ReportPrintTask` record with status **pending**. Since no print service is connected to the demo, the task stays pending, but you can inspect the full record and see how status tracking works. Completed print tasks are purged automatically after a configurable retention period.

---

<h3 id="limits" style="color: #203671; margin-top: 2.2em;">Demo Limitations</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px;">What</th>
      <th style="text-align: left; padding: 6px 10px;">Behaviour in the demo</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Print tasks</td>
      <td style="padding: 6px 10px;">Created and traceable, but no real printer connected, tasks remain <strong>pending</strong>. Completed tasks are purged automatically.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Mail dispatch</td>
      <td style="padding: 6px 10px;">Mailers and Mail Templates can be created freely and a mailing can be configured end to end. The log mailer records the mail instead of sending it; whether a real mail is delivered depends on the SMTP account you enter, the demo has no mailbox of its own.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Live SQL connections</td>
      <td style="padding: 6px 10px;">Connection Configs can be created and tested, but no external databases are reachable from the demo environment.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">User management</td>
      <td style="padding: 6px 10px;">The demo account does not have admin rights, user accounts and permissions cannot be managed.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;">Data persistence</td>
      <td style="padding: 6px 10px;">Uploaded templates, history records, and other data may be reset periodically.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Ready to go further?</strong> See <a href="/books/veloxfactory/page/meet-the-frontend">Meet the Frontend</a> for a full walkthrough of the UI, or <a href="/books/veloxfactory/page/meet-the-api">Meet the API</a> for complete API documentation.
</div>

</div>

# Open Source Attribution

VeloxFactory and its companion Background Printing Service are built on open source software. We are grateful to the authors and contributors of the following packages.

<div id="bkmrk-" style="text-align: justify;">---

</div>### VeloxFactory

<div id="bkmrk-package-license-desc" style="text-align: justify;"><table style="width: 100%; border-collapse: collapse;"><thead><tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;"><th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Package</th><th style="text-align: left; padding: 6px 10px;">License</th><th style="text-align: left; padding: 6px 10px;">Description</th></tr></thead><tbody><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[laravel/framework](https://github.com/laravel/framework)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">The Laravel PHP framework routing, ORM, queues, and the application foundation.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[laravel/horizon](https://github.com/laravel/horizon)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">Queue manager and dashboard for the Redis queues that run thumbnails, mail dispatch and the nightly purge jobs.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[laravel/reverb](https://github.com/laravel/reverb)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">First-party Laravel WebSocket server used to broadcast print task events in real time.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[laravel/sanctum](https://github.com/laravel/sanctum)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">API token authentication for the VeloxFactory REST API.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[laravel/tinker](https://github.com/laravel/tinker)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">REPL for the Laravel application.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[livewire/livewire](https://github.com/livewire/livewire)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">Full-stack component framework for the VeloxFactory frontend.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[opcodesio/log-viewer](https://github.com/opcodesio/log-viewer)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">In-browser Laravel log viewer.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[knuckleswtf/scribe](https://github.com/knuckleswtf/scribe)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">Automatic API documentation generator.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[phpoffice/phpspreadsheet](https://github.com/PHPOffice/PhpSpreadsheet)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">Spreadsheet library used to build the xlsx export that a report mailing can attach alongside the PDF.</td></tr><tr><td style="padding: 6px 10px; white-space: nowrap;">[quilhasoft/jasperphp](https://github.com/quilhasoft/jasperphp)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">Pure-PHP JasperReports renderer the engine that compiles `.jrxml` templates and produces PDFs without a Java runtime.</td></tr></tbody></table>

---

</div>### Background Printing Service

<div id="bkmrk-package-license-desc-1" style="text-align: justify;"><table style="width: 100%; border-collapse: collapse;"><thead><tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;"><th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Package</th><th style="text-align: left; padding: 6px 10px;">License</th><th style="text-align: left; padding: 6px 10px;">Description</th></tr></thead><tbody><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[PdfiumViewer](https://github.com/pvginkel/PdfiumViewer)</td><td style="padding: 6px 10px;">Apache 2.0</td><td style="padding: 6px 10px;">.NET wrapper around the PDFium library used to render and send PDFs to Windows printers.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[PDFium](https://pdfium.googlesource.com/pdfium/)</td><td style="padding: 6px 10px;">BSD 3-Clause</td><td style="padding: 6px 10px;">Google's PDF rendering engine, bundled as a native binary via `PdfiumViewer.Native.x86_64.v8-xfa`.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[RestSharp](https://github.com/restsharp/RestSharp)</td><td style="padding: 6px 10px;">Apache 2.0</td><td style="padding: 6px 10px;">HTTP client library for all API communication with VeloxFactory.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap;">[Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json)</td><td style="padding: 6px 10px;">MIT</td><td style="padding: 6px 10px;">JSON serialisation and deserialisation for API responses and WebSocket messages.</td></tr><tr><td style="padding: 6px 10px; white-space: nowrap;">[Serilog](https://github.com/serilog/serilog)</td><td style="padding: 6px 10px;">Apache 2.0</td><td style="padding: 6px 10px;">Structured logging to console and rolling file (`Serilog.Sinks.Console`, `Serilog.Sinks.File`).</td></tr></tbody></table>

</div>

# Get VeloxFactory

<div style="text-align: justify;">

Interested in using VeloxFactory in your own environment? Get in touch, we'll figure out the right setup together.

---

<h3 style="color: #203671; margin-top: 2.2em;">Contact</h3>

<table style="width: 100%; border-collapse: collapse;">
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 8px 10px; color: #525E5A; width: 140px;">Name</td>
      <td style="padding: 8px 10px;">Benjamin Fischer</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 8px 10px; color: #525E5A;">E-Mail</td>
      <td style="padding: 8px 10px;"><a href="mailto:bfischer@kiwi-software.dev">bfischer@kiwi-software.dev</a></td>
    </tr>
    <tr>
      <td style="padding: 8px 10px; color: #525E5A;">Website</td>
      <td style="padding: 8px 10px;"><a href="https://www.kiwi-software.dev" target="_blank">www.kiwi-software.dev</a></td>
    </tr>
  </tbody>
</table>

---

<h3 style="color: #203671; margin-top: 2.2em;">What to Expect</h3>

Drop a short message explaining your use case, what you need to generate, how often, and whether you'd prefer self-hosted or managed. You'll hear back within one business day.

If you want to explore the product first, the live demo is open:

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Want to try it first?</strong> A live demo is available at <a href="https://demo.veloxfactory.dev" target="_blank">demo.veloxfactory.dev</a>.
<p style="margin: 6px 0 0 0;"><strong>Login:</strong> <code>demo@veloxfactory.dev</code> &nbsp;·&nbsp; <strong>Password:</strong> <code>demo</code></p>
</div>

</div>

<div style="text-align: center; margin-top: 40px; color: #84C441; font-family: 'Montserrat', sans-serif; font-weight: 700;">
Smarter Code. Frischer Blick.
</div>

# Report Configurations

From a JRXML in Jaspersoft Studio to a Report Config in VeloxFactory, including the data adapters.

# Creating reports in Jaspersoft Studio

<div style="text-align: justify;">

VeloxFactory renders reports defined as `.jrxml` files — the native format of JasperReports. These files are designed in **Jaspersoft Studio**, a free desktop IDE built specifically for this purpose. This page explains how to set up a `.jrxml` file so that VeloxFactory can analyse it correctly, register all its parts, and render it reliably.

[![jaspersoft-studio-overview.png](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/scaled-1680-/jaspersoft-studio-overview.png)](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/jaspersoft-studio-overview.png)

---

<h3 style="color: #203671; margin-top: 2.2em;">Jaspersoft Studio</h3>

Jaspersoft Studio is an Eclipse-based visual report designer. You use it to lay out the report template — define what data goes where on the page, how it is formatted, and which inputs the report expects. The resulting `.jrxml` file is then uploaded to VeloxFactory, which takes over everything from there: storing it, analysing it, connecting it to a data source, and rendering it on demand.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Use version 6.21.5.</strong> This is the version compatible with VeloxFactory. Download it from the <a href="https://community.jaspersoft.com/download-jaspersoft/community-edition/jaspersoft-studio_windows_6.x" target="_blank">Jaspersoft Community</a>. Reports created with a significantly newer version may use features that VeloxFactory's render engine does not support.
</div>

The division of responsibilities between Jaspersoft Studio and VeloxFactory is clear:

- **Jaspersoft Studio** defines the layout, the parameters, the fields, and the visual design of the report.
- **VeloxFactory** manages the SQL query, the data connection, and the actual rendering at runtime.

This means you will see a `<queryString>` element in Jaspersoft Studio — but as explained below, you leave it empty. VeloxFactory supplies the query separately.

---

<h3 style="color: #203671; margin-top: 2.2em;">The Report Name</h3>

Every Jaspersoft Studio project has a **Report Name** — the `name` attribute on the root `<jasperReport>` element. This is not just a filename: VeloxFactory reads it directly from the `.jrxml` on upload and stores it as `report_name` on the `ReportConfig`.

```xml
<jasperReport ... name="A5_KanBan" ...>
```

This name must be **unique** across all report configurations in VeloxFactory. If you try to upload a `.jrxml` whose report name already exists, the upload will be rejected. The same applies to the file name itself.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Set a descriptive, unique report name in Jaspersoft Studio before uploading.</strong> You can change it in Jaspersoft Studio via <em>File → Report Properties → Report Name</em>, or directly in the XML. Changing it after upload requires re-uploading the file.
</div>

[![jaspersoft-studio-report-properties.png](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/scaled-1680-/jaspersoft-studio-report-properties.png)](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/jaspersoft-studio-report-properties.png)

---

<h3 style="color: #203671; margin-top: 2.2em;">Parameters</h3>

Parameters are **input values** passed into the report at render time. They are used inside the report layout via `$P{PARAMETER_NAME}` expressions — for example to display a customer name in the title, filter by a date range, or pass a document number into a barcode expression.

You define parameters in Jaspersoft Studio via the **Report Inspector → Parameters** section. Each parameter has a name and a Java data type.

[![jaspersoft-studio-parameter-properties.png](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/scaled-1680-/jaspersoft-studio-parameter-properties.png)](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/jaspersoft-studio-parameter-properties.png)

<h4 style="color: #203671; margin-top: 1.4em;">Custom Properties: exampleValue and required</h4>

VeloxFactory reads two custom properties from each parameter definition: `exampleValue` and `required`. These are not standard Jaspersoft features — they are custom `<property>` elements you add manually to the parameter in the `.jrxml`. VeloxFactory's analyser (`JasperFunctions::analyzeReportFile`) extracts them on upload.

| Property | Value type | Purpose |
|---|---|---|
| `exampleValue` | string | A representative value used when rendering a preview of the report without real data. Also pre-fills the parameter input in the frontend render form. |
| `required` | boolean (`true` / `false`) | Whether this parameter must be provided in every render request. VeloxFactory rejects render requests that are missing a required parameter. |

Both properties can also be set or updated manually in VeloxFactory after upload — they do not have to come from the `.jrxml`. But embedding them in the file means they are automatically picked up every time the report is uploaded or re-uploaded.

To add these properties in Jaspersoft Studio, open the parameter in the **Report Inspector**, switch to the **Properties** panel, and add a new custom property via the green **+** button.

[![jaspersoft-studio-parameter-custom-properties.png](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/scaled-1680-/jaspersoft-studio-parameter-custom-properties.png)](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/jaspersoft-studio-parameter-custom-properties.png)

<h4 style="color: #203671; margin-top: 1.4em;">Supported Data Types</h4>

VeloxFactory supports the following Java class types for parameters. The type determines how the input field is rendered in the frontend and how the value is handled at render time.

| Java class | Frontend input | Notes |
|---|---|---|
| `java.lang.String` | Text field | General-purpose text input |
| `java.lang.Boolean` | Toggle | Rendered as a checkbox/toggle switch |
| `java.lang.Short` | Integer input | Range: −32,768 to 32,767 |
| `java.lang.Integer` | Integer input | Range: −2,147,483,648 to 2,147,483,647 |
| `java.lang.Long` | Integer input | 64-bit integer |
| `java.lang.Float` | Decimal input | Single-precision floating point |
| `java.lang.Double` | Decimal input | Double-precision floating point |
| `java.math.BigDecimal` | Decimal input | Arbitrary-precision decimal; preferred for monetary values |
| `java.sql.Date` | Date picker | Date only (no time) |
| `java.util.Date` | Date picker | Date only (no time) |
| `java.sql.Time` | Time picker | Time only (HH:mm) |
| `java.sql.Timestamp` | Date + time picker | Combined date and time (datetime-local) |

---

<h3 style="color: #203671; margin-top: 2.2em;">Fields</h3>

Fields represent the **data columns** that populate the report's detail band — the repeating section that produces one row of output per data record. Each field corresponds to a column in the SQL query result (when using a live connection) or a key in the data array delivered at render time via the API.

You define fields in Jaspersoft Studio via the **Report Inspector → Fields** section. In the report layout, you reference them via `$F{FIELD_NAME}` expressions inside text fields in the detail band.

[![jaspersoft-studio-fields.png](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/scaled-1680-/jaspersoft-studio-fields.png)](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/jaspersoft-studio-fields.png)

<h4 style="color: #203671; margin-top: 1.4em;">Custom Property: exampleValue</h4>

Fields support one custom property: `exampleValue`. It works the same way as for parameters — a representative value used when rendering a preview of the report without a live data connection. VeloxFactory collects these example values and assembles a synthetic data row from them for preview rendering.

You add `exampleValue` to a field the same way as for parameters: open the field in the **Report Inspector**, go to the **Properties** tab, and add the custom property.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Set meaningful example values for all fields.</strong> Without them, VeloxFactory cannot generate a preview or thumbnail for the report configuration. The values do not need to be real data — they just need to be type-compatible and representative enough to make the preview look sensible.
</div>

Fields support the same Java data types as parameters (see the table above). Use the type that matches the column type your SQL query or data array will produce.

---

<h3 style="color: #203671; margin-top: 2.2em;">Using Parameters as SQL Variables</h3>

When a `ReportConfig` has a `ReportConnectionConfig` assigned, VeloxFactory executes the SQL query defined in the report configuration against that live database connection. Parameters passed in the render request are available as **named bindings** inside that query — using the standard `:parameterName` syntax from Laravel's Eloquent database layer.

This means you can reference any parameter directly in your SQL to filter, sort, or limit the result set:

```sql
SELECT
    article_number  AS articleNumber,
    description,
    moq,
    delivery_time   AS deliveryTime,
    supplier,
    barcode
FROM articles
WHERE article_number = :P_ARTICLE_NUMBER
```

In this example, `:P_ARTICLE_NUMBER` is replaced at query execution time with the value of the `P_ARTICLE_NUMBER` parameter from the render request. The binding is handled natively by PDO — values are passed as proper prepared statement parameters, not interpolated as strings.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Only parameters that are actually referenced in the query are bound.</strong> VeloxFactory scans the query for <code>:name</code> placeholders before execution and silently drops any parameters that are not needed. Passing extra parameters in the render request will never cause a query error.
</div>

The parameter name in the query binding must match the parameter name exactly as defined in the `.jrxml` — including case. For example, a parameter named `P_ARTICLE_NUMBER` in the report must be referenced as `:P_ARTICLE_NUMBER` in the SQL query.

You can use as many parameters as needed across `WHERE`, `ORDER BY`, `LIMIT`, or any other clause that accepts a value binding. Note that named bindings cannot be used for identifiers like table or column names — only for values.

---

<h3 style="color: #203671; margin-top: 2.2em;">Resources (Images and Logos)</h3>

If your report contains images — a company logo, a header graphic, a product photo — these are defined as **resource parameters** in the `.jrxml`. VeloxFactory detects them automatically on upload and creates a `ReportResource` record for each one.

The naming convention is strict: **every resource parameter must start with `P_RESOURCE_`** and must have the Java class `java.lang.String`. At render time, VeloxFactory replaces the parameter value with the actual file path of the uploaded image on the server.

```xml
<!-- Resource parameter: logo image -->
<parameter name="P_RESOURCE_LOGO" class="java.lang.String">
    <defaultValueExpression><![CDATA["C:/VeloxFactory/Logo_Dark.png"]]></defaultValueExpression>
</parameter>
```

In the report layout, you then bind this parameter to an image element:

```xml
<image>
    <reportElement x="460" y="100" width="84" height="50" uuid="..."/>
    <imageExpression><![CDATA[$P{P_RESOURCE_LOGO}]]></imageExpression>
</image>
```

The `<defaultValueExpression>` is used only in Jaspersoft Studio for design-time preview purposes. VeloxFactory ignores it at render time — it always uses the uploaded file path instead. You can point it to a local file on your design machine.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Resource parameters are not treated as regular parameters in VeloxFactory.</strong> They do not appear in the Parameters section — they appear in the Resources section. In the vast majority of render requests you do not pass them at all; VeloxFactory fills them in automatically from the linked resource file.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Exception: per-render overrides.</strong> A render request can optionally supply <code>resourceOverrides</code> to swap a resource parameter's image for that single call only — a local path, a remote <code>http(s)://</code> URL, or a Base64 upload — without changing the report configuration's linked default. This is an opt-in override on top of the linked resource described above, not a replacement for it: a default file or <code>CommonReportResource</code> still has to be linked before the report can render at all. See <a href="/books/veloxfactory/page/rendering-with-our-powerful-api">Rendering with our powerful API</a> for the full reference.
</div>

After uploading the `.jrxml`, you must go to the **Resources** section of the report configuration in VeloxFactory and upload the actual image file for each detected resource. A report cannot be rendered until all its resources have files assigned.

[![jaspersoft-studio-image-resource.png](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/scaled-1680-/jaspersoft-studio-image-resource.png)](https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-05/jaspersoft-studio-image-resource.png)

---

<h3 style="color: #203671; margin-top: 2.2em;">The Detail Band</h3>

The detail band is the repeating section of the report — the part that outputs one row per data record. If your report displays a list of items, a table, or any kind of repeating structure, it lives in the detail band.

VeloxFactory checks whether a detail band is present when analysing the `.jrxml`:

- If a detail band exists, VeloxFactory expects data to be provided at render time — either via a live SQL connection or via the `data` array in the API request.
- If no detail band exists, the report is treated as a **static layout** — no data is needed, only parameters.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>If you add a detail band, it must contain at least one text field element that uses a field expression (<code>$F{...}</code>).</strong> A detail band that exists but displays no field data will be rejected on upload. VeloxFactory uses the presence of text fields in the band as a basic integrity check.
</div>

Reports without a detail band are perfectly valid — they are useful for documents like cover pages, summary sheets, or any report whose content is entirely driven by parameters rather than repeated rows.

---

<h3 style="color: #203671; margin-top: 2.2em;">The SQL Query</h3>

Jaspersoft Studio has a built-in `<queryString>` element where you would normally write the SQL query for the report. **In VeloxFactory, this element is ignored.** The SQL query is instead defined and stored directly in VeloxFactory as part of the `ReportConfig` — not in the `.jrxml` file.

This means you should **leave the `<queryString>` empty** when creating a `.jrxml` for VeloxFactory:

```xml
<queryString>
    <![CDATA[]]>
</queryString>
```

You write the actual SQL query in VeloxFactory after uploading the report, in the **Query** field of the report configuration. This design keeps the query where it can be managed, versioned, and changed without touching the report template file.

The query result columns must match the field names you defined in the `.jrxml`. For example, if your report has a field named `articleNumber`, your SQL query must return a column called `articleNumber`.

---

<h3 style="color: #203671; margin-top: 2.2em;">The Data Adapter</h3>

Jaspersoft Studio uses **Data Adapters** to connect to a live database so you can preview your report during design. This is entirely a design-time feature — the data adapter you configure in Jaspersoft Studio has no effect in VeloxFactory and is not stored in the `.jrxml`.

At render time, VeloxFactory always uses its own internal array-based adapter to pass data to JasperReports. Whether that data comes from an SQL query executed against a `ReportConnectionConfig` or from a data array in the API request, VeloxFactory always handles the data handoff itself.

You still benefit from configuring a data adapter in Jaspersoft Studio during development — it lets you see a realistic preview while designing the layout. Just be aware that the adapter configuration stays local to your machine.

---

<h3 style="color: #203671; margin-top: 2.2em;">Annotated Example</h3>

The following is a complete, minimal `.jrxml` that demonstrates everything discussed on this page. It is the actual `A5_KanBan` report included in the VeloxFactory demo. Inline comments explain each relevant element.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.21.5 -->

<jasperReport
    xmlns="http://jasperreports.sourceforge.net/jasperreports"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="..."

    name="A5_KanBan"         <!-- Report Name — becomes report_name in VeloxFactory. Must be unique. -->
    pageWidth="595"          <!-- Page dimensions in Jasper pixels (1px = 1/72 inch).              -->
    pageHeight="842"         <!-- VeloxFactory converts these to mm on upload.                       -->
    ...>

    <!-- ═══════════════════════════════════════════════════════════════════
         RESOURCE PARAMETER
         Name starts with P_RESOURCE_ and class is java.lang.String.
         VeloxFactory detects this as a resource (image/logo), not a parameter.
         The defaultValueExpression points to a local file for Studio preview —
         VeloxFactory replaces it at render time with the uploaded file path.
         ═══════════════════════════════════════════════════════════════════ -->
    <parameter name="P_RESOURCE_LOGO" class="java.lang.String">
        <defaultValueExpression><![CDATA["C:/VeloxFactory/Logo_Dark.png"]]></defaultValueExpression>
    </parameter>

    <!-- ═══════════════════════════════════════════════════════════════════
         REGULAR PARAMETER
         P_ARTICLE_NUMBER is a user-supplied input value passed at render time.
         exampleValue  → used for preview rendering and pre-fills the frontend form.
         required      → omitting this parameter in a render request will cause a 422 error.
         ═══════════════════════════════════════════════════════════════════ -->
    <parameter name="P_ARTICLE_NUMBER" class="java.lang.String">
        <property name="exampleValue" value="4561287-154"/>
        <property name="required"     value="true"/>
    </parameter>

    <!-- ═══════════════════════════════════════════════════════════════════
         QUERY STRING — leave empty.
         The SQL query is defined in VeloxFactory, not here.
         ═══════════════════════════════════════════════════════════════════ -->
    <queryString>
        <![CDATA[]]>
    </queryString>

    <!-- ═══════════════════════════════════════════════════════════════════
         FIELDS
         Each field maps to a column in the SQL result or a key in the data array.
         exampleValue → used for preview rendering when no live data is available.
         ═══════════════════════════════════════════════════════════════════ -->
    <field name="articleNumber" class="java.lang.String">
        <property name="exampleValue" value="1868745-584"/>
    </field>

    <field name="description" class="java.lang.String">
        <property name="exampleValue" value="Packing Carton Size 1 - 200x150x50mm"/>
    </field>

    <field name="moq" class="java.lang.Integer">
        <property name="exampleValue" value="250"/>
    </field>

    <field name="deliveryTime" class="java.lang.String">
        <property name="exampleValue" value="3 Days"/>
    </field>

    <field name="supplier" class="java.lang.String">
        <property name="exampleValue" value="Ninghao Packaging"/>
    </field>

    <field name="barcode" class="java.lang.String">
        <property name="exampleValue" value="5698532145712"/>
    </field>

    <!-- ═══════════════════════════════════════════════════════════════════
         DETAIL BAND
         The repeating section — one iteration per data row.
         Must contain at least one <textField> using a $F{...} expression,
         otherwise VeloxFactory rejects the file on upload.
         The image element uses $P{P_RESOURCE_LOGO} — VeloxFactory resolves
         this to the uploaded resource file at render time.
         ═══════════════════════════════════════════════════════════════════ -->
    <detail>
        <band height="167" splitType="Stretch">

            <!-- Field values referenced via $F{...} expressions -->
            <textField>
                <textFieldExpression><![CDATA[$F{articleNumber}]]></textFieldExpression>
            </textField>

            <textField>
                <textFieldExpression><![CDATA[$F{description}]]></textFieldExpression>
            </textField>

            <!-- Resource image: bound to the P_RESOURCE_LOGO parameter -->
            <image>
                <imageExpression><![CDATA[$P{P_RESOURCE_LOGO}]]></imageExpression>
            </image>

        </band>
    </detail>

</jasperReport>
```

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>This example has been simplified for clarity.</strong> A real `.jrxml` contains many additional attributes, layout elements, and Studio-specific property annotations. The elements shown here are the ones VeloxFactory actively reads and acts on — everything else is passed through to JasperReports as-is.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Pre-Upload Checklist</h3>

Before uploading a `.jrxml` to VeloxFactory, verify the following:

- The **Report Name** (`name` attribute on `<jasperReport>`) is set, descriptive, and unique.
- All **resource parameters** follow the `P_RESOURCE_` naming convention and have class `java.lang.String`.
- All **regular parameters** have `exampleValue` and `required` custom properties set where applicable.
- All **fields** have an `exampleValue` custom property set.
- The **`<queryString>`** is empty — the SQL is managed in VeloxFactory.
- If the report has a **detail band**, it contains at least one `<textField>` with a `$F{...}` expression.
- The report **previews correctly in Jaspersoft Studio** using the local data adapter — this confirms the layout and expressions are valid before upload.

</div>

# Managing reports in VeloxFactory

<div style="text-align: justify;">

A report in VeloxFactory is more than a file. It is a fully managed configuration - with its own parameters, data fields, image resources, SQL query, data connection, preview images, rendering history, and print records. This page walks through the complete lifecycle of a report configuration, from upload to print.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.index.png</code><br>Report Config overview, the landing page after login, with context badges and thumbnails.
</div>

---

<h3 id="the-reportconfig" style="color: #203671; margin-top: 2.2em;">The ReportConfig - Central Master Data</h3>

The `ReportConfig` is the core entity in VeloxFactory. Every report you manage is a `ReportConfig` record, and everything else in the system either belongs to it or references it. A single `ReportConfig` brings together:

- The **`.jrxml` template file** stored on disk
- Its **parameters** - input values passed at render time
- Its **fields** - data columns that populate the detail band
- Its **resources** - graphic assets (images, logos) embedded in the template
- Its **SQL query** - defined and managed directly in VeloxFactory
- Its **data connection** - the live database to query (optional)
- Its **context** - an organisational label for grouping
- Its **preview and thumbnail images** - generated from example data

Nothing renders without a `ReportConfig`. Nothing prints without one either. It is the starting point for every operation in VeloxFactory.

---

<h3 id="lifecycle" style="color: #203671; margin-top: 2.2em;">The Report Configuration Lifecycle</h3>

```
Upload .jrxml
     │
     ▼
Auto-analysis
(parameters, fields, resources detected and created)
     │
     ▼
Complete the configuration
(upload resource files, set example values, write SQL query, assign connection)
     │
     ▼
Generate preview
(renders with example data, stores preview + thumbnail)
     │
     ▼
Ready to render
```

Each step is described in detail below.

---

<h3 id="uploading" style="color: #203671; margin-top: 2.2em;">Uploading a Report</h3>

When you upload a `.jrxml` file, VeloxFactory immediately analyses it and builds the initial configuration automatically. The following is extracted from the file:

- **Report name** - from the `name` attribute on `<jasperReport>`. Must be unique.
- **Page dimensions** - width and height, converted from Jasper pixels to millimetres.
- **Detail band presence** - whether the report has a repeating data section.
- **Parameters** - all non-resource parameters, including their data types and any `exampleValue` / `required` custom properties set in the `.jrxml`.
- **Fields** - all data fields, including their data types and `exampleValue` custom properties.
- **Resources** - all parameters following the `P_RESOURCE_` naming convention (image assets).

The result is a fully structured `ReportConfig` record with all its child records in place, but not yet complete. Resource files still need to be uploaded, and the SQL query still needs to be written if a live data connection is used.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Report name and file name must be unique.</strong> VeloxFactory will reject an upload if a <code>ReportConfig</code> with the same report name or the same file name already exists.
</div>

The file name may contain letters, digits, dot, hyphen and underscore, must end in `.jrxml` and is limited to 120 characters. Directory parts are not accepted.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.create.png</code><br>Report Config create form with the JRXML upload, context and data adapter.
</div>

---

<h3 id="completing" style="color: #203671; margin-top: 2.2em;">Completing the Configuration</h3>

After upload, the report configuration is ready but not yet fully operational. The following steps complete it:

<h4 style="color: #203671; margin-top: 1.4em;">Upload Resource Files</h4>

If the report contains image resources (detected as `P_RESOURCE_` parameters), each one requires an actual file to be uploaded. VeloxFactory cannot render the report until all resource files are in place.

Alternatively, a resource can be linked to a `CommonReportResource` - a shared asset reused across multiple reports, such as a company logo. Linking is permanent: the resource's own file is deleted and the common file is used in its place.

A Common Report Resource serves a second purpose as well: a PNG or JPEG uploaded there can be placed into a mail template as an inline graphic, referenced by its name. The same logo therefore covers the report and the mail it is sent with. See [Mail Templates](/books/veloxfactory/page/mail-templates-subject-body-and-whitelabeling).

Resource files are images: `png`, `jpg`, `jpeg`, `gif` or `webp`. VeloxFactory checks the actual content, not the extension, and refuses a file whose bytes are not one of those formats.

The file name is normalised before it is stored. Any directory part is dropped, inner dots become hyphens (`logo.v2.png` is stored as `logo-v2.png`), everything outside letters, digits, hyphen and underscore becomes a hyphen, and the name part is cut at 80 characters. The stored name is what the report refers to, so a renamed file shows up under its normalised name in the resource list.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The linked/uploaded file is the default, not the only option.</strong> A single render request can override a resource's image for that call alone via <code>resourceOverrides</code> - a local path, a remote URL, or a Base64 upload - without touching this configuration. The file uploaded here is still required as the fallback. See <a href="/books/veloxfactory/page/rendering-with-our-powerful-api">Rendering with our powerful API</a>.
</div>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.edit.png</code><br>Report Config edit view with parameters, fields and resources.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Review Parameters and Fields</h4>

VeloxFactory picks up `exampleValue` and `required` custom properties from the `.jrxml` automatically on upload. If these were not set in Jaspersoft Studio, or if you need to adjust them, you can do so directly in the configuration.

Every parameter and field should have an example value set before generating a preview.

<h4 style="color: #203671; margin-top: 1.4em;">Write the SQL Query and Assign a Connection</h4>

If the report fetches live data from a database, assign a `ReportConnectionConfig` and write the SQL query in the **Query** field. The query is stored in VeloxFactory, not in the `.jrxml`.

Parameters are available as named bindings in the query (`:PARAMETER_NAME`). See [Creating reports in Jaspersoft Studio](/books/veloxfactory/page/creating-reports-in-jaspersoft-studio) for details on how parameter binding works.

A connection is not required if the report has no detail band, or if data will be delivered in the render request itself.

---

<h3 id="preview" style="color: #203671; margin-top: 2.2em;">Generating a Preview</h3>

Once all resource files are uploaded and all parameters and fields have example values, you can generate a preview. VeloxFactory renders the report using the stored example values, no live data needed, and stores the result as a base64-encoded PDF and a thumbnail image on the `ReportConfig`.

The preview is used in the report list as a visual card and as a quick sanity check that the template renders correctly. It is also what the `useExampleValues` flag triggers during a render request, useful for testing without providing real data.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Preview generation will fail if any resource file is missing or any example value is not set.</strong> VeloxFactory checks all three conditions - resources, parameter example values, and field example values - before attempting to render.
</div>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.edit.preview.png</code><br>Report Config edit view with the generated preview and thumbnail.
</div>

---

<h3 id="history-records" style="color: #203671; margin-top: 2.2em;">Report History Records</h3>

Every render request can optionally create a `ReportHistoryRecord` - a full log entry of what was requested and what was returned. This is controlled by the `createHistoryRecord` flag in the render request body and is **off by default**.

When enabled, the history record captures:

- The exact request payload sent to the render endpoint
- The full API response
- The rendered PDF (Base64-encoded)
- A thumbnail of the first page (generated asynchronously in the background)
- The rendering status (`ok`, `render_fail`, `error`, `unknown`)
- The trace ID for cross-referencing with logs

History records are valuable for traceability, you can see exactly what was rendered, when, with what data, and what the result was. From a history record, you can also dispatch a reprint or send the document by mail directly.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Downloads follow the read permission.</strong> Downloading a report template, a resource file, a rendered PDF or an xlsx export requires the read permission for that resource. Being signed in is not enough.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">When to Skip History Records</h4>

History records are entirely optional. There are two good reasons to leave them off:

**Performance and storage.** Storing the full PDF, request payload, and response for every render adds up. For high-frequency rendering where traceability is not needed, skipping history records keeps the database lean.

**Data sensitivity.** A history record stores the complete render payload, including all parameters and data passed to the report. If that data is sensitive (personal data, financial figures, medical information), you may not want it persisted on the server at all. Omitting `createHistoryRecord` from the render request ensures nothing is logged.

<h4 style="color: #203671; margin-top: 1.4em;">Retention</h4>

History records are automatically purged after a configurable number of days. The retention period is set via the `PURGE_HISTORY_DAYS` environment variable (default: 30 days). Purging runs automatically as a background job, no manual intervention required.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-history-record.index.filtered.png</code><br>Report History Record overview with filters applied and status badges.
</div>

---

<h3 id="mail-tasks" style="color: #203671; margin-top: 2.2em;">Report Mail Tasks</h3>

The second way out of VeloxFactory is a mail. A render request can carry a `mailing` segment, and a document that was rendered earlier can be mailed from its history record at any time. Either way a `ReportMailTask` is created, logging recipients, subject, body and attachments, exactly as a print task logs printer and copies.

What a mailing needs is set up once under *Configuration*: a **Mailer** holds the SMTP account, a **Mail Template** holds subject and body with placeholders. The PDF is attached, and on request an xlsx export of the same rows the report was rendered with. Sending is synchronous by default, or handed to the queue.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>A mail task blocks its history record from being deleted, just like a print task does.</strong> Mail tasks have their own retention setting, <code>PURGE_MAILTASKS_DAYS</code>, and are purged before the history records they point at. Full detail on <a href="/books/veloxfactory/page/report-mailing-sending-renderings-by-mail">Report Mailing</a>.
</div>

---

<h3 id="print-tasks" style="color: #203671; margin-top: 2.2em;">Report Print Tasks</h3>

A `ReportPrintTask` sends a rendered PDF to a physical printer. Print tasks are created as part of a render request, you render and dispatch to a printer in a single call, by setting `createPrintTask: true` and providing a `printerName`.

Print tasks are always linked to a `ReportHistoryRecord`. This means creating a print task also creates a history record (regardless of whether `createHistoryRecord` is explicitly set), so the printed document is always traceable.

<h4 style="color: #203671; margin-top: 1.4em;">How Printing Works</h4>

VeloxFactory does not communicate with printers directly. Instead, it creates a `ReportPrintTask` record and notifies a separate print service - a lightweight C# application running on or near the target machine - which picks up the task and executes the print job.

There are two modes of delivery:

**WebSocket (push).** If a `broadcastId` is included in the render request, VeloxFactory broadcasts a `ReportPrintTaskCreated` event via WebSocket (Laravel Reverb) the moment the task is created. The print service subscribes to that channel and reacts immediately. This is the recommended mode for real-time printing, the task reaches the printer within milliseconds of the render completing.

**Polling (pull).** Without a `broadcastId`, no broadcast is sent. The print service must poll the API for new tasks in `pending` status. This works fine for less time-sensitive workflows.

<h4 style="color: #203671; margin-top: 1.4em;">Print Task Status</h4>

| Status | Meaning |
|---|---|
| <span style="color: #203671; font-weight: 600;">pending</span> | Created, waiting for the print service to pick it up |
| <span style="color: #349b31; font-weight: 600;">printed</span> | Print job executed and confirmed by the print service |
| <span style="color: #c0392b;">error</span> | Print service reported a failure |
| <span style="color: #525E5A;">unknown</span> | Status could not be determined |

The print service reports status back to VeloxFactory via the API after executing the job. The `error_message` field on the task record contains the failure detail if printing did not succeed.

<h4 style="color: #203671; margin-top: 1.4em;">Copies</h4>

The `numberOfCopies` field is passed to the print service as the requested number of printed copies. It defaults to `1` if not specified. VeloxFactory always renders the PDF exactly once, the print service is responsible for duplicating the output on the printer side.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Print tasks also have configurable retention.</strong> They are automatically purged after <code>PURGE_PRINTTASKS_DAYS</code> days (default: 30). Like history record purging, this runs in the background without any manual action.
</div>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-print-task.index.png</code><br>Report Print Task overview with status badges, printer and the reset action.
</div>

---

<h3 id="deletion" style="color: #203671; margin-top: 2.2em;">Deleting a Report Configuration</h3>

A `ReportConfig` can only be deleted when no `ReportHistoryRecord` or `ReportPrintTask` references it. VeloxFactory will reject a deletion request while any such records exist.

When a `ReportConfig` is deleted, the following is removed along with it: the `.jrxml` file from disk, all resource files, and all parameter and field records. The deletion is atomic, if any step fails, the entire operation is rolled back.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>To delete a ReportConfig that has history records or print tasks, those records must be removed first.</strong> Once the retention period has passed and the automatic purge has run, or once the records are manually deleted, the ReportConfig can be removed.
</div>

</div>

# Data adapters for dyn. data control

<div style="text-align: justify;">

Reports that display repeating data - lists, tables, card grids - need a data source. VeloxFactory supports two ways to supply that data at render time: a live **SQL connection** that queries a database automatically, or a **dynamic array** delivered directly in the render request. Understanding when to use which approach, and how each one works, is key to getting the most out of VeloxFactory.

---

<h3 id="two-approaches" style="color: #203671; margin-top: 2.2em;">Two Approaches, One Result</h3>

| | SQL Connection | Dynamic Array |
|---|---|---|
| **Data source** | Live database, queried at render time | JSON array in the render request body |
| **Who fetches the data?** | VeloxFactory | The calling application |
| **Connection config needed?** | Yes | No |
| **SQL query needed?** | Yes | No |
| **Best for** | Reports where VeloxFactory has direct DB access | Reports where the caller already has the data |

Both approaches produce the same result: a populated report. The choice depends on where your data lives and who is best placed to retrieve it.

Reports without a detail band - purely static layouts driven by parameters - need neither.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The rows are used beyond the PDF.</strong> Whatever ends up as field data, fetched by an SQL query or delivered in the request, is also what an xlsx mail attachment is built from, and what the <code>[data.first.&lt;column&gt;]</code> placeholders address. For an SQL-driven report that means a mail can be addressed to a recipient the query itself returned. See <a href="/books/veloxfactory/page/report-mailing-sending-renderings-by-mail">Report Mailing</a>.
</div>

---

<h3 id="sql-connection" style="color: #203671; margin-top: 2.2em;">SQL Connections</h3>

A `ReportConnectionConfig` defines a live database connection that VeloxFactory uses to fetch data at render time. When assigned to a `ReportConfig`, VeloxFactory executes the configured SQL query against that connection, takes the result rows, and feeds them as field data into the report.

<h4 style="color: #203671; margin-top: 1.4em;">Setting Up a Connection</h4>

A connection config holds the credentials and driver settings for one database. Supported drivers are MySQL, MariaDB, PostgreSQL, and Microsoft SQL Server.

Before a connection can be assigned to a report, it must be **tested and approved**. VeloxFactory runs a test query against the database to verify connectivity, only connections with a passing test are available in the ReportConfig assignment dropdown.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>The database must be reachable from the VeloxFactory server.</strong> For databases in separate networks, use an encrypted VPN tunnel (WireGuard or OpenVPN). Do not expose database ports to the public internet. See <a href="#">Configuration and Data Models</a> for network requirements.
</div>

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>MSSQL connections verify the server certificate.</strong> Earlier releases accepted any certificate. A connection to a server with a self-signed certificate now fails until either a valid certificate is in place or <code>DB_TRUST_SERVER_CERTIFICATE=true</code> is set explicitly in the instance's <code>.env</code>. Check your existing connections before you upgrade.
</div>

The host of a connection config is checked when it is saved. Loopback and link-local addresses are refused. An instance can narrow this further with `OUTBOUND_HOST_ALLOWLIST`, a comma separated list of the hosts that connections and mailers may point at; an empty list means everything except loopback and link-local. A failed connection test answers with a generic message, the reason is in the application log.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Give the connection a read-only database user.</strong> No <code>FILE</code> privilege on MySQL and MariaDB, no <code>pg_read_server_files</code> on PostgreSQL, <code>db_datareader</code> without <code>xp_cmdshell</code> on MS SQL Server. The permission <code>report-config:update</code> lets a user write the query, which in practice means read access to everything that database user can reach.
</div>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-connection-config.index.png</code><br>Report Connection Config overview with driver badges and the approval status.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Writing the SQL Query</h4>

The SQL query is written and stored in VeloxFactory, not in the `.jrxml`. It lives on the `ReportConfig` record and is executed against the assigned connection at render time.

The query must return columns whose names match **exactly** the field names defined in the `.jrxml`. For a report with fields `articleNumber`, `description`, and `moq`, the query must alias its columns accordingly:

```sql
SELECT
    art_no          AS articleNumber,
    art_description AS description,
    min_order_qty   AS moq,
    delivery_days   AS deliveryTime,
    supplier_name   AS supplier,
    barcode
FROM articles
ORDER BY art_no ASC
```

Column names are case-sensitive. `articleNumber` and `articlenumber` are not the same field.

<h4 style="color: #203671; margin-top: 1.4em;">What a Report Query May Contain</h4>

A report query is a single read statement. VeloxFactory checks it when the Report Config is saved and refuses anything else:

- It must begin with `SELECT` or `WITH`.
- It must not contain a semicolon, so a second statement cannot be appended.
- It must not use constructs that read files, write files or run commands on the database server: `INTO OUTFILE`, `INTO DUMPFILE`, `LOAD_FILE`, `LOAD DATA`, `BULK INSERT`, `OPENROWSET`, `OPENDATASOURCE`, `xp_cmdshell`, `sp_OACreate`, `pg_read_file` and their relatives.
- At most 20,000 characters.

Parameters stay parameters. They are bound by the driver, never pasted into the statement, which is why a value can safely contain quotes or a semicolon.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Existing queries keep running, but may no longer save.</strong> The check runs when a Report Config is written, not when it renders. A query that predates this release and contains a semicolon, even inside a string literal, is refused the next time the Report Config is edited. Rewrite it with <code>CHAR(59)</code> or a parameter.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Using Parameters as SQL Variables</h4>

Parameters passed in the render request are available as named bindings in the SQL query using the `:PARAMETER_NAME` syntax. VeloxFactory scans the query for `:name` placeholders before execution and binds only the parameters that are actually referenced, extras are silently ignored.

This makes it straightforward to filter, sort, or paginate the result set based on render-time input:

```sql
-- Filter by article number
SELECT
    art_no          AS articleNumber,
    art_description AS description,
    min_order_qty   AS moq
FROM articles
WHERE art_no = :P_ARTICLE_NUMBER
```

```sql
-- Date range filter with two parameters
SELECT
    order_id        AS orderId,
    customer_name   AS customerName,
    order_date      AS orderDate,
    total_amount    AS totalAmount
FROM orders
WHERE order_date BETWEEN :P_DATE_FROM AND :P_DATE_TO
ORDER BY order_date ASC
```

```sql
-- Wildcard search
SELECT
    art_no          AS articleNumber,
    art_description AS description
FROM articles
WHERE art_description LIKE CONCAT('%', :P_SEARCH_TERM, '%')
```

The render request for the date range example would look like this:

```json
POST /api/v1/report-config/OrderList/render

{
  "outputType": "base64",
  "parameters": {
    "P_DATE_FROM": "2024-01-01",
    "P_DATE_TO":   "2024-03-31"
  },
  "data": [],
  "createHistoryRecord": true,
  "createPrintTask": false
}
```

<h4 id="parameter-promotion" style="color: #203671; margin-top: 1.4em;">Parameter Promotion from Query Results</h4>

There is a powerful pattern worth knowing: if a SQL result column has the same name as a registered parameter on the report, VeloxFactory automatically **promotes** that value from the data rows into the parameters map, before the report renders.

This means you can derive parameter values directly from the database without having to pass them in the render request. The query does the lookup; the result feeds both the detail band and the header parameters in a single call.

Consider a report that prints a picking list for a warehouse order. The header shows the order number, the customer name, and the warehouse location, all parameters. The detail band shows the individual line items, fields. Normally you would have to fetch the order header separately and pass it as parameters. With parameter promotion, a single query can deliver everything:

```sql
-- First row drives the header parameters, all rows drive the detail band.
-- P_ORDER_NUMBER, P_CUSTOMER_NAME, and P_WAREHOUSE match registered parameter
-- names and will be promoted automatically. The remaining columns stay as field data.

SELECT
    o.order_number      AS P_ORDER_NUMBER,
    c.customer_name     AS P_CUSTOMER_NAME,
    w.location_code     AS P_WAREHOUSE,
    ol.sku              AS sku,
    ol.description      AS description,
    ol.quantity         AS quantity,
    ol.bin_location     AS binLocation
FROM orders o
JOIN customers c   ON c.id = o.customer_id
JOIN warehouses w  ON w.id = o.warehouse_id
JOIN order_lines ol ON ol.order_id = o.id
WHERE o.order_number = :P_ORDER_NUMBER
ORDER BY ol.bin_location ASC
```

The render request only needs the order number:

```json
POST /api/v1/report-config/PickingList/render

{
  "outputType": "base64",
  "parameters": {
    "P_ORDER_NUMBER": "ORD-2024-00451"
  },
  "data": [],
  "createHistoryRecord": true,
  "createPrintTask": false
}
```

VeloxFactory executes the query, detects that `P_ORDER_NUMBER`, `P_CUSTOMER_NAME`, and `P_WAREHOUSE` match registered parameter names, moves their values from the first data row into the parameters map, and renders the report with a populated header and a fully populated detail band, all from one query, one request.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Parameter promotion reads from every row, but only the last encountered value is kept.</strong> For consistent results, make sure promoted columns carry the same value across all rows, as in the example above, where the order header data is identical on every line item row.
</div>

---

<h3 id="dynamic-array" style="color: #203671; margin-top: 2.2em;">Dynamic Array</h3>

When no `ReportConnectionConfig` is assigned, VeloxFactory expects the data to arrive in the render request itself, as a JSON array in the `data` field. Each object in the array represents one row in the detail band, with keys matching the field names defined in the `.jrxml`.

This approach is ideal when the calling application already has the data in memory, when the data comes from a source VeloxFactory cannot connect to directly, or when the data structure is too dynamic to express in a fixed SQL query.

A complete render request with inline data looks like this:

```json
POST /api/v1/report-config/A5_KanBan/render

{
  "outputType": "base64",
  "parameters": {
    "P_ARTICLE_NUMBER": "4561287-154"
  },
  "data": [
    {
      "articleNumber": "4561287-154",
      "description":   "Packing Carton Size 1 - 200x150x50mm",
      "moq":           250,
      "deliveryTime":  "3 Days",
      "supplier":      "Ninghao Packaging",
      "barcode":       "5698532145712"
    }
  ],
  "createHistoryRecord": false,
  "createPrintTask": false
}
```

For reports that print one item per page, the `data` array typically contains a single object. For list or table reports, it contains one object per row.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Data types in the array must be compatible with the field types defined in the <code>.jrxml</code>.</strong> A field declared as <code>java.lang.Integer</code> expects a JSON number, not a string. Pass values in their native JSON type, numbers as numbers, booleans as booleans.
</div>

---

<h3 id="no-data" style="color: #203671; margin-top: 2.2em;">Static Reports - No Data Needed</h3>

Reports without a detail band require neither a connection config nor a data array. The entire output is driven by parameters alone. Common examples: cover pages, certificates, summary headers, QR code labels, or any document where the layout is fixed and all variable content comes from a handful of input values.

For these reports, the render request simply omits `data` entirely:

```json
POST /api/v1/report-config/CertificateOfConformity/render

{
  "outputType": "url",
  "parameters": {
    "P_PRODUCT_NAME":   "Industrial Bearing 6205-2RS",
    "P_BATCH_NUMBER":   "BAT-2024-0077",
    "P_ISSUE_DATE":     "2024-03-15",
    "P_INSPECTOR_NAME": "M. Fischer"
  },
  "createHistoryRecord": true,
  "createPrintTask": false
}
```

</div>

# Rendering reports in VeloxFactory

Turning a Report Config into a document: from the frontend, from the API, and what every render leaves behind.

# Rendering from the frontend

<div style="text-align: justify;">

Every report configuration in VeloxFactory has a built-in **Generate PDF** page: a small workstation for one report template that renders it directly from the browser, without writing a single line of code or touching the API. It is the fastest way to produce a PDF, test a configuration, or trigger a print job on demand.

The page follows one principle: **settings once, then only the data.** How the output should be produced, preview or print, which printer, how many copies, whether a history record is kept, is decided once per report and then stays out of the way. From there, every render is nothing but filling in the values that actually change and pressing one button.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>generate-pdf.page.png</code><br>Generate PDF page with the header row (report name, settings summary, Output Settings button), parameter table and the action row.
</div>

---

<h3 id="opening" style="color: #203671; margin-top: 2.2em;">Opening the Generate PDF Page</h3>

There are two ways to reach the page. In the report configuration overview, each row has an action menu that contains **Generate PDF**, taking you straight to the render page. Alternatively, open a report configuration and use **Generate PDF** from within the report view.

The header row shows the report name and, as a compact summary line, the active data adapter and the stored output settings: mode, printer, copies, broadcast ID and whether a history record is created. The data adapter is either the assigned `ReportConnectionConfig` (including driver and database) or **dyn. Array** when no SQL connection is configured, and it decides which input sections appear further down.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-config.index.action-menu.png</code><br>Report configuration overview, opened action menu with Generate PDF and Download.
</div>

---

<h3 id="settings" style="color: #203671; margin-top: 2.2em;">Output Settings</h3>

The first time you open the page for a report, the **Output Settings** dialog opens by itself, because VeloxFactory does not yet know what you want to do with the result. Afterwards it only opens when you click the **Output Settings** button in the header.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Setting</th>
      <th style="text-align: left; padding: 6px 10px;">Default</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Mode</td>
      <td style="padding: 6px 10px;">Preview and download only</td>
      <td style="padding: 6px 10px;">Decides what happens with the rendered PDF, see the table below.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Printer</td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">Printer picker over the active printer master data, free text is allowed as well. Required in both printing modes.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Copies</td>
      <td style="padding: 6px 10px;">1</td>
      <td style="padding: 6px 10px;">Copies passed to the print service, between 1 and 100. Does not trigger multiple renders.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Broadcast ID</td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">WebSocket channel of the print service. Filled in automatically when the chosen printer has one stored.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Create History Record</td>
      <td style="padding: 6px 10px;"><span style="color: #349b31; font-weight: 600;">On</span></td>
      <td style="padding: 6px 10px;">Saves request, response, PDF and thumbnail of every render to the report history.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Send results via Mail</td>
      <td style="padding: 6px 10px;"><span style="color: #525E5A;">Off</span></td>
      <td style="padding: 6px 10px;">Mails every render of this report, see <a href="#bkmrk-mailing-the-result">Mailing the result</a> below.</td>
    </tr>
  </tbody>
</table>

The three modes:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Mode</th>
      <th style="text-align: left; padding: 6px 10px;">Preview</th>
      <th style="text-align: left; padding: 6px 10px;">Print task</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Preview and download only</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">no</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Print only, no preview</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">yes</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Preview and print</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">yes</td>
    </tr>
  </tbody>
</table>

In *Preview and download only* the printer and broadcast fields are hidden, there is nothing to print to. In the two printing modes a printer is mandatory: saving without one fails and the dialog reopens with everything you entered still in place.

Settings are kept **per report configuration in your session**. Two report templates therefore keep independent settings, they survive leaving the page and coming back, and they end when your session does. **Clear settings** at the bottom of the dialog removes them, the dialog then opens by itself on the next visit.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The settings are yours, not the report's.</strong> They live in your session, so a colleague configuring the same report for a different printer never affects your workstation.
</div>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>generate-pdf.settings-modal.png</code><br>Output Settings dialog with mode dropdown, printer picker, copies, broadcast ID, history toggle, the mailing block and the Clear settings button.
</div>

<h4 id="bkmrk-mailing-the-result" style="color: #203671; margin-top: 1.4em;">Mailing the Result</h4>

**Send results via Mail** turns every render of this report into a mail as well. While the switch is on, the dialog shows the mailing fields:

| Field | Description |
|---|---|
| Mailer / Mail Template | The SMTP account and the template the mail is built from. Only active records are offered. |
| To, CC, BCC | Comma separated address lists. Placeholders are allowed, an address containing one is checked after the render, not in the dialog. |
| Contact Name | Free text for a personal greeting, available in the template as `[contactName]`. |
| Attach PDF Report | On by default, with an optional name the attachment carries in the mail. |
| Attach Excel Report | Attaches an xlsx built from the rendered rows, with an optional attachment name and a switch for the parameter block above the table. |
| Send in background | Hands the mail to the queue instead of waiting for the SMTP server. |

The values stay in the form even while the switch is off, so turning mailing off for one render loses nothing. After the render the notification says what happened to the mail: sent, queued, or a warning with the reason. A mail that fails never turns a successful render into an error.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The mailing block only appears when you can use it.</strong> It needs the permission to create mail tasks plus at least one active mailer and one active mail template. The same block sits in the Scan2Print configuration and on a render job in the Job Scheduler. Details on <a href="/books/veloxfactory/page/report-mailing-sending-renderings-by-mail">Report Mailing</a>.
</div>

---

<h3 id="parameters" style="color: #203671; margin-top: 2.2em;">Report Parameters</h3>

If the report defines parameters, a parameter input table is shown. Each parameter gets its own typed input field, derived automatically from the Java class declared in the `.jrxml`:

- A `java.lang.String` parameter becomes a text field.
- A `java.sql.Date` parameter becomes a date picker.
- A `java.lang.Integer` parameter becomes a number input with integer constraints.
- A `java.lang.Boolean` parameter becomes a toggle switch.
- And so on for all supported types.

Parameters marked as **required** in the report configuration must be filled in before the form can be submitted. Optional parameters can be left empty, VeloxFactory silently drops empty parameter values and does not include them in the render request.

---

<h3 id="fields" style="color: #203671; margin-top: 2.2em;">Report Lines - Manual Data Entry</h3>

The **Report Lines** section only appears when the report has **no SQL connection assigned**, i.e. when the data adapter is `dyn. Array`. In this case, VeloxFactory has no database to query, so the detail band data must be entered manually in the browser.

The section shows a table with one column per field defined in the report. Each cell contains a typed input matching the field's data type. You fill in one row of values per data record you want to appear in the report.

To add more rows, use the **Add Row** button, it clones the input row and appends a new empty one. Individual rows (except the first) can be removed with the delete button on the right. Empty fields are not transferred to the render request.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>When a SQL connection is assigned, the Report Lines section is hidden.</strong> VeloxFactory fetches the data automatically from the database using the configured query, no manual input needed.
</div>

---

<h3 id="typing" style="color: #203671; margin-top: 2.2em;">Less typing: prefill, example values, clear</h3>

Three small helpers remove most of the repetitive work when the same report is rendered again and again:

- **Prefill from the last run.** Parameters and report lines come back filled with the values of your previous render in this session. The values are stored on every submit, successful or not, so a failed render can be corrected instead of retyped.
- **Fill example values.** Fills every parameter and the first line row with the example values stored in the report configuration. This is the frontend counterpart of the API's `useExampleValues`, useful for a quick smoke test of a new template.
- **Clear inputs.** Empties all parameters and removes every line row except the first one.

---

<h3 id="resources" style="color: #203671; margin-top: 2.2em;">Resources</h3>

If the report has image resources, a collapsible **Resources** section is available on the page. It shows a preview thumbnail of each resource file, its parameter name, and its file name. Resources are handled automatically at render time, you do not interact with them during rendering. The section is informational only, confirming which image files are currently assigned.

---

<h3 id="generating" style="color: #203671; margin-top: 2.2em;">Generating</h3>

Once the values are in place, click **Generate**. There is no confirmation dialog: the stored output settings already say what is supposed to happen with the result. The request is processed synchronously, the page waits for the result and displays it immediately.

---

<h3 id="result" style="color: #203671; margin-top: 2.2em;">The Result</h3>

<h4 style="color: #203671; margin-top: 1.4em;">Success</h4>

In the two modes with a preview, the page shows a green confirmation banner, a **Download PDF** button and the generated PDF as an inline preview, sized to the report's actual page dimensions. If a history record was created, the download button serves the stored file, otherwise the PDF is offered straight from the response.

In **Print only**, no preview is rendered at all. The banner then reads *Sent to &lt;printer&gt; (&lt;n&gt;x)*, which is the whole point of that mode: a station that prints and moves on.

If a history record was created, a **View History** button links to the new `ReportHistoryRecord`. If a print task was dispatched, a **View Print Task** button links to the `ReportPrintTask` where you can follow its status.

<div style="border-left: 4px solid #f0ad4e; background: #fdf9f2; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Reloading the page renders the report again.</strong> The result lives in the response, so a browser reload repeats the render, including a new print task if the mode creates one.
</div>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>generate-pdf.result.png</code><br>Generate PDF page after a render, with the result block and the inline PDF.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Errors</h4>

If the render fails, the page shows a red error banner listing all error messages returned by VeloxFactory, and everything you entered stays on screen. Common causes are missing required parameters, a SQL query that returns no rows, or a resource file that was removed after the configuration was last saved.

If a history record was created before the error occurred, the failed attempt is recorded including the error details, which is useful for diagnosing what went wrong.

</div>

# Rendering with our powerful API

<div style="text-align: justify;">

Everything the Generate PDF page does in the browser, the API does programmatically, with more control, lower overhead, and the same rendering engine underneath. A single `POST` request renders a report, optionally logs the result, and optionally dispatches a print job, all in one call.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>api-docs.png</code><br>The interactive API reference at /docs, generated by Scribe.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">The Render Endpoint</h3>

```
POST /api/v1/report-config/{id}/render
```

The `{id}` segment accepts either the numeric ID of the `ReportConfig` or its **report name**, the name set in Jaspersoft Studio and stored in VeloxFactory. Both of these are equivalent:

```
POST /api/v1/report-config/1/render
POST /api/v1/report-config/A5_KanBan/render
```

Using the report name is convenient for integrations: it stays stable even if the database record is recreated, and it makes the request self-documenting.

---

<h3 style="color: #203671; margin-top: 2.2em;">Request Body</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Field</th>
      <th style="text-align: left; padding: 6px 10px;">Type</th>
      <th style="text-align: left; padding: 6px 10px;">Required</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>outputType</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;">✓</td><td style="padding: 6px 10px;">Output format: <code>base64</code>, <code>url</code>, or <code>none</code>. See below.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>parameters</code></td><td style="padding: 6px 10px;">object</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Key-value map of parameter names to values. Required parameters must be present or the request is rejected.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>resourceOverrides</code></td><td style="padding: 6px 10px;">object</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Per-render override for <code>P_RESOURCE_*</code> image parameters, keyed by parameter name. See <a href="#bkmrk-dynamic-resource-overrides">Dynamic Resource Overrides</a> below.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>data</code></td><td style="padding: 6px 10px;">array</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Array of field objects, one per detail band row. Each object's keys must match the report's field names. Only needed when no SQL connection is configured. Can include per-row images, see <a href="#bkmrk-per-row-images-via-data-fields">Per-Row Images via Data Fields</a> below.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>createHistoryRecord</code></td><td style="padding: 6px 10px;">boolean</td><td style="padding: 6px 10px;">✓</td><td style="padding: 6px 10px;">Whether to create a <code>ReportHistoryRecord</code> for this render. Stores the full request, response, and rendered PDF.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>createPrintTask</code></td><td style="padding: 6px 10px;">boolean</td><td style="padding: 6px 10px;">✓</td><td style="padding: 6px 10px;">Whether to dispatch the rendered PDF to the print service.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>printerName</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;">if print task</td><td style="padding: 6px 10px;">Target printer name. Required when <code>createPrintTask</code> is <code>true</code>.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>numberOfCopies</code></td><td style="padding: 6px 10px;">integer</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Number of copies passed to the print service. Defaults to <code>1</code>. VeloxFactory always renders once, the print service handles duplication.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>broadcastId</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">WebSocket channel ID. If provided, VeloxFactory broadcasts a <code>ReportPrintTaskCreated</code> event when the print task is created. Omit to rely on polling.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>useExampleValues</code></td><td style="padding: 6px 10px;">boolean</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Use the stored example values instead of supplying <code>parameters</code> and <code>data</code>. Useful for testing. API-only - not available in the frontend. See below.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>laconicResponse</code></td><td style="padding: 6px 10px;">boolean</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Return only the essential output fields instead of the full response. Reduces payload size significantly for high-frequency rendering. See below.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>traceId</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Custom trace identifier for this request. Auto-generated (UUID) if not provided. Must be unique across all history records if supplied.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>mailing</code></td><td style="padding: 6px 10px;">object</td><td style="padding: 6px 10px;"></td><td style="padding: 6px 10px;">Send the rendering by mail after a successful render. See <a href="#bkmrk-mailing-the-rendering">Mailing the rendering</a> below.</td></tr>
  </tbody>
</table>

---

<h3 style="color: #203671; margin-top: 2.2em;" id="bkmrk-dynamic-resource-overrides">Dynamic Resource Overrides</h3>

Every `P_RESOURCE_*` image parameter (a logo, a product photo, a line-art reference) normally resolves to whichever file is uploaded or linked on the `ReportConfig` itself. `resourceOverrides` lets a single render request swap that image out, without touching the report configuration, useful for per-customer branding, per-item product photos, or any other case where the image genuinely varies from call to call.

```json
POST /api/v1/report-config/A4_AssemblyBOM/render

{
  "outputType": "base64",
  "parameters": { "P_PROJECT_NUMBER": "PRJ-2026-0417" },
  "resourceOverrides": {
    "P_RESOURCE_ASSEMBLY_IMAGE": { "path": "https://cdn.example.com/parts/4471.png" }
  },
  "createHistoryRecord": false,
  "createPrintTask": false
}
```

`resourceOverrides` is an object keyed by resource parameter name. Each entry supports exactly one of two modes:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Key</th>
      <th style="text-align: left; padding: 6px 10px;">Type</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>path</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;">A path to a readable file <strong>inside the resources folder</strong> of the instance, or an <code>https://</code> URL. Remote URLs are fetched directly by the render engine at render time. A path that leaves the resources folder and any scheme other than <code>https</code> are refused.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>base64</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;">Base64-encoded image bytes. VeloxFactory decodes them, writes a temporary file for the duration of the render, and deletes it immediately afterward.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>fileName</code></td><td style="padding: 6px 10px;">string</td><td style="padding: 6px 10px;">Optional, used only alongside <code>base64</code> to infer the file extension.</td></tr>
  </tbody>
</table>

**Base64 example:**

```json
{
  "outputType": "base64",
  "resourceOverrides": {
    "P_RESOURCE_ASSEMBLY_IMAGE": {
      "base64": "iVBORw0KGgoAAAANSUhEUgAA...",
      "fileName": "part-4471.png"
    }
  },
  "createHistoryRecord": false,
  "createPrintTask": false
}
```

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>A resource must still be linked or uploaded on the <code>ReportConfig</code> before it can be overridden.</strong> <code>resourceOverrides</code> replaces the resolved path for a single render, it does not exempt a <code>P_RESOURCE_*</code> parameter from needing a default resource in place. Rendering still fails with the usual <code>"Not all resources for this report have been uploaded yet!"</code> error if no file or <code>CommonReportResource</code> is linked at all.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Do not set the same parameter in both <code>parameters</code> and <code>resourceOverrides</code>.</strong> VeloxFactory rejects the request with a <code>422</code> if a key appears in both, to avoid ambiguous precedence.
</div>

```json
{
  "success": false,
  "errors": ["Parameter 'P_RESOURCE_LOGO' is set in both 'parameters' and 'resourceOverrides' - use only one."],
  "status": 422
}
```

Other validation errors follow the same pattern: an unknown resource parameter name, a `path` that leaves the resources folder or cannot be read, a URL that is not `https://`, or invalid `base64` all return a descriptive `422` rather than failing deep inside the render engine.

---

<h3 style="color: #203671; margin-top: 2.2em;" id="bkmrk-per-row-images-via-data-fields">Per-Row Images via Data Fields</h3>

`resourceOverrides` covers one image per report-level `P_RESOURCE_*` parameter, the same picture used across the whole render. Some reports need the opposite: a different image for every row of the detail band, for example a product photo per line item in a parts list or an order. That case does not need `resourceOverrides` at all, it works through the `data` array directly.

An `<image>` element in a `.jrxml` is not limited to `$P{...}` parameter expressions, it accepts a field expression just like any text field:

```xml
<image>
    <reportElement x="0" y="24" width="120" height="100" uuid="..."/>
    <imageExpression><![CDATA[$F{partPhoto}]]></imageExpression>
</image>
```

With that in place, every object in the render request's `data` array can carry its own `partPhoto` value, resolved independently per row, using the same three delivery styles as `resourceOverrides`:

```json
POST /api/v1/report-config/PartsList/render

{
  "outputType": "base64",
  "data": [
    { "partNumber": "4471-A", "partPhoto": "https://cdn.example.com/parts/4471.jpg" },
    { "partNumber": "4471-B", "partPhoto": "/var/www/resources/parts/4471b.png" },
    { "partNumber": "4471-C", "partPhoto": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." }
  ],
  "createHistoryRecord": false,
  "createPrintTask": false
}
```

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Field value</th>
      <th style="text-align: left; padding: 6px 10px;">Behaviour</th>
    </tr>
  </thead>
  <tbody>
    <tr><td style="padding: 6px 10px; white-space: nowrap;">Local path</td><td style="padding: 6px 10px;">Read directly from the filesystem by the render engine.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>http(s)://</code> URL</td><td style="padding: 6px 10px;">Fetched directly by the render engine at render time, one request per row.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>data:image/png;base64,...</code></td><td style="padding: 6px 10px;">Decoded inline, no temporary file involved.</td></tr>
    <tr><td style="padding: 6px 10px; white-space: nowrap;"><code>data:image/jpg;base64,...</code></td><td style="padding: 6px 10px;">Same as PNG. Note the MIME token must be exactly <code>image/jpg</code>.</td></tr>
  </tbody>
</table>

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong><code>data:image/jpeg;base64,...</code> is not recognized.</strong> The render engine only matches the literal prefixes <code>data:image/png;base64,</code> and <code>data:image/jpg;base64,</code> - the far more common <code>image/jpeg</code> MIME token is not one of them and the image silently fails to render. Always encode JPEG uploads with the <code>image/jpg</code> token in the data URI, regardless of what the source tool actually calls the file.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>No request-level syntax needed.</strong> Unlike <code>resourceOverrides</code>, per-row images are just regular field values, there is no validation, no linked-resource requirement, no restriction on the location, and no separate object in the request body. Whatever the field resolves to at render time is handed straight to the render engine.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Output Types</h3>

The `outputType` field controls how, or whether, the rendered PDF is returned.

**`base64`**: The PDF is Base64-encoded and returned inline in `output.reportPdfBase64`. No file is written to disk. This is the most common choice for integrations that process the PDF immediately.

**`url`**: The PDF is saved to the VeloxFactory history storage and a URL pointing to that file is returned in `output.reportUrl`. Useful when the calling application needs to hand off a link rather than handle raw bytes.

**`none`**: No PDF data is returned at all. Valid only when `createPrintTask` is `true`, the PDF is rendered internally and handed to the print service without being exposed in the response. Use this when the response payload is irrelevant and you only care about getting the document to the printer.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong><code>outputType: none</code> requires <code>createPrintTask: true</code>.</strong> Requesting output type <code>none</code> without a print task is rejected with a validation error, there would be nothing to do with the rendered PDF. A <code>mailing</code> segment does not replace the print task here, and a mail on a render that produced no PDF goes out without its attachment.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">useExampleValues - API-only Testing Mode</h3>

When `useExampleValues: true` is set, VeloxFactory ignores any `parameters` and `data` in the request body and instead uses the example values stored on the `ReportConfig`. This is the same data used to generate the report preview in the frontend.

It is a convenient way to verify that a report renders correctly after configuration changes, no test data needs to be assembled:

```json
POST /api/v1/report-config/A5_KanBan/render

{
  "outputType": "base64",
  "useExampleValues": true,
  "createHistoryRecord": false,
  "createPrintTask": false
}
```

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong><code>useExampleValues</code> is an API-only feature.</strong> The Generate PDF page in the browser always requires parameters and data to be entered manually. For frontend testing, use the example values from the report configuration edit page.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">laconicResponse - Minimal Output</h3>

By default, a successful render response includes the full `ReportConfig` record, the input parameters and data echoed back, and any linked `ReportHistoryRecord` and `ReportPrintTask`. For many production integrations, this detail is unnecessary, the caller only needs the PDF.

Setting `laconicResponse: true` strips the response down to the essentials: just the `traceId` and the `output` block. Everything else (`input`, `reportConfig`, `reportHistoryRecord`, `reportPrintTask`) is omitted.

The two response shapes are shown in detail in the Response Structure section below.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The laconic mode also suppresses <code>reportMeta</code> in error responses.</strong> If a render fails in laconic mode, the error response contains only the error messages, the field and parameter metadata is not included.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">A Complete Request</h3>

Here is a full render request for a KanBan label, dynamic array data, a parameter, history logging enabled, print task dispatched via WebSocket:

```json
POST /api/v1/report-config/A5_KanBan/render

{
  "outputType": "base64",
  "parameters": {
    "P_ARTICLE_NUMBER": "4561287-154"
  },
  "data": [
    {
      "articleNumber": "4561287-154",
      "description":   "Packing Carton Size 1 - 200x150x50mm",
      "moq":           250,
      "deliveryTime":  "3 Days",
      "supplier":      "Ninghao Packaging",
      "barcode":       "5698532145712"
    }
  ],
  "createHistoryRecord": true,
  "createPrintTask": true,
  "printerName": "WarehousePrinter01",
  "numberOfCopies": 1,
  "broadcastId": "Standard",
  "laconicResponse": false
}
```

---

<h3 style="color: #203671; margin-top: 2.2em;">Response Structure</h3>

<h4 style="color: #203671; margin-top: 1.4em;">Full Response</h4>

The full response (default, `laconicResponse: false` or omitted) includes the rendered output, the echoed input, the full `ReportConfig` snapshot, and any created `ReportHistoryRecord` and `ReportPrintTask`:

```json
{
  "success": true,
  "count": 1,
  "data": {
    "model": "ReportRendering",
    "traceId": "ec1e29de-7aca-4c59-9722-ae9edc7d24d7",
    "input": {
      "parameters": { "P_ARTICLE_NUMBER": "4561287-154" },
      "data": [
        {
          "articleNumber": "4561287-154",
          "description":   "Packing Carton Size 1 - 200x150x50mm",
          "moq":           250,
          "deliveryTime":  "3 Days",
          "supplier":      "Ninghao Packaging",
          "barcode":       "5698532145712"
        }
      ]
    },
    "output": {
      "reportPdfFileName": "a7dd0ea5-85fd-481c-998b-fa9819c2e84c.pdf",
      "reportPdfBase64":   "JVBERi0xLjQ..."
    },
    "reportConfig": {
      "model": "ReportConfig",
      "id": 1,
      "name": "A5_KanBan",
      ...
    },
    "reportHistoryRecord": {
      "model": "ReportHistoryRecord",
      "id": 4,
      "traceId": "ec1e29de-7aca-4c59-9722-ae9edc7d24d7",
      "outputType": "Base64",
      "status": "Ok"
    },
    "reportPrintTask": {
      "model": "ReportPrintTask",
      "id": 4,
      "traceId": "ec1e29de-7aca-4c59-9722-ae9edc7d24d7",
      "broadcastId": "Standard",
      "printerName": "WarehousePrinter01",
      "numberOfCopies": 1,
      "status": "Pending",
      "errorMessage": null
    }
  },
  "links": { "first": null, "last": null, "prev": null, "next": null },
  "meta": {
    "traceId": "ec1e29de-7aca-4c59-9722-ae9edc7d24d7",
    "pagination": {
      "currentPage": 1,
      "lastPage": 1,
      "perPage": 1,
      "total": 1,
      "from": 1,
      "to": 1,
      "hasMorePages": false,
      "path": "https://your-instance/api/v1/report-config/A5_KanBan/render"
    }
  },
  "status": 200
}
```

<h4 style="color: #203671; margin-top: 1.4em;">Laconic Response</h4>

With `laconicResponse: true`, the response contains only what is needed to retrieve the PDF:

```json
{
  "success": true,
  "count": 1,
  "data": {
    "model": "ReportRendering",
    "traceId": "555d073b-a630-4096-acd1-643b85ed5cc9",
    "output": {
      "reportPdfFileName": "8de016b5-4cf9-423a-9575-8c3155e35410.pdf",
      "reportPdfBase64":   "JVBERi0xLjQ..."
    }
  },
  "links": { "first": null, "last": null, "prev": null, "next": null },
  "meta": {
    "traceId": "555d073b-a630-4096-acd1-643b85ed5cc9",
    "pagination": {
      "currentPage": 1,
      "lastPage": 1,
      "perPage": 1,
      "total": 1,
      "from": 1,
      "to": 1,
      "hasMorePages": false,
      "path": "https://your-instance/api/v1/report-config/A5_KanBan/render"
    }
  },
  "status": 200
}
```

The `traceId` is always included, it links this render to any created history record or print task, making it useful for cross-referencing even in laconic mode.

The envelope around `data` is the one every endpoint uses: a render reports a single page in `meta.pagination` and carries no page links. See [Response Structure](/books/veloxfactory/page/meet-the-api) on Meet the API.

---

<h3 style="color: #203671; margin-top: 2.2em;" id="bkmrk-mailing-the-rendering">Mailing the Rendering</h3>

A render request can carry an optional `mailing` segment. Once the render succeeded, VeloxFactory sends an HTML mail through a configured mailer, using a configured mail template, with the PDF attached and, on request, an xlsx export of the same data.

```json
POST /api/v1/report-config/DeliveryNote/render

{
  "outputType": "base64",
  "parameters": { "P_ORDER_NO": "4711" },
  "createHistoryRecord": true,
  "createPrintTask": false,
  "mailing": {
    "mailer": "Office SMTP",
    "mailTemplate": "Report Delivery",
    "to": ["[data.first.customerEmail]"],
    "contactName": "[data.first.customerName]",
    "includePdf": true,
    "pdfFileName": "Order_[parameters.P_ORDER_NO]",
    "includeExcel": true,
    "includeParameters": true,
    "sendAsync": false
  }
}
```

`mailer` and `mailTemplate` take the ID or the unique name of the master data record. Recipients, contact name and attachment names accept the same placeholders as the mail template, which is what makes the segment useful for reports fed by an SQL adapter: `[data.first.<column>]` addresses the rows the query actually fetched, so the recipient is a result of the render rather than an input to it.

The created mail task is returned in `reportMailTask`, next to `reportPrintTask`.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>A failed mailing never fails the render.</strong> Whatever goes wrong with the mail, the render response stays a success and the detail lands in <code>meta.mailing</code>. The one combination refused up front is <code>outputType: "preview"</code> together with <code>mailing</code>, a preview render deletes its own file immediately and has nothing to attach.
</div>

The full reference, including every field, the placeholder rules, the rate limits and how to mail a rendering that already exists, is on <a href="/books/veloxfactory/page/report-mailing-sending-renderings-by-mail">Report Mailing</a>.

---

<h3 id="on-a-schedule" style="color: #203671; margin-top: 2.2em;">Running It on a Schedule</h3>

Everything on this page describes a request somebody makes. The same body can be handed to the Job Scheduler instead, which then makes the request itself, on a crontab expression, as the user who created the schedule and with one of that user's API tokens.

The payload of a render job is this body verbatim, with two differences: `traceId` is left out, because every run generates its own, and `outputType` is limited to `base64`, `url` and `none`. Parameters and data rows additionally accept placeholders such as `[now.date]` or `[uuid]`, resolved at the start of each run.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ See <a href="/books/veloxfactory/page/render-jobs-a-render-request-with-a-clock-attached">Render Jobs</a> for the schedule around it, and <a href="/books/veloxfactory/page/the-job-scheduler-work-that-happens-without-you">The Job Scheduler</a> for how runs are fired and logged.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Errors</h3>

<h4 style="color: #203671; margin-top: 1.4em;">Validation Errors - HTTP 422</h4>

Missing required fields, an invalid `outputType` value, or a missing `printerName` when a print task is requested all produce a `422` response with an `errors` array describing the violations.

Required parameters that are not present in the request also return a `422`, one error message per missing parameter:

```json
{
  "success": false,
  "errors": [
    "Parameter P_DATE_FROM is required.",
    "Parameter P_DATE_TO is required."
  ],
  "meta": { "traceId": "..." },
  "status": 422
}
```

<h4 style="color: #203671; margin-top: 1.4em;">Render Errors - HTTP 400</h4>

If the request passes validation but the renderer itself fails, for example an empty data array for a report with a detail band, an SQL query that returns no rows, a type mismatch between field values and declared Java types, or a broken SQL query, the response comes back with HTTP `400` and a `success: false` payload.

In full (non-laconic) mode, a `reportMeta` block is included in the `meta` object alongside the `traceId`. This snapshot lists the report's fields, parameters, and resources at the time of the failure, useful for diagnosing mismatches between the request payload and what the report actually expects:

```json
{
  "success": false,
  "errors": [
    "No data delivered (or fetched via SQL using parameters) while data deliverance is mandatory for reports with detail bands or SQL queries."
  ],
  "meta": {
    "traceId": "08deac82-274d-4f56-b9d0-d9fdb6280f8f",
    "reportMeta": {
      "resourceList": [
        { "parameterName": "P_RESOURCE_LOGO", "fileName": "Logo_Dark.png" }
      ],
      "parameterList": [
        { "parameterName": "P_ARTICLE_NUMBER", "dataType": "java.lang.String" }
      ],
      "fieldList": [
        { "fieldName": "articleNumber", "dataType": "java.lang.String" },
        { "fieldName": "description",   "dataType": "java.lang.String" },
        { "fieldName": "moq",           "dataType": "java.lang.Integer" },
        { "fieldName": "deliveryTime",  "dataType": "java.lang.String" },
        { "fieldName": "supplier",      "dataType": "java.lang.String" },
        { "fieldName": "barcode",       "dataType": "java.lang.String" }
      ]
    }
  },
  "status": 400
}
```

If a `ReportHistoryRecord` was requested (`createHistoryRecord: true`), it is still created even when the render fails, the error is recorded in the history entry, which makes it possible to review failed renders from the frontend alongside successful ones.

</div>

# The concept of Report History Records

Every render request tells VeloxFactory what to produce. A `ReportHistoryRecord` remembers exactly what was asked for, what came back, and what the result looked like, permanently, until you decide otherwise. It is the foundation for traceability, debugging, and on-demand reprinting in VeloxFactory.

<div id="bkmrk-screenshot-placehold" style="text-align: justify;"><div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525e5a;">**Screenshot placeholder:** `report-history-record.index.filtered.png`  
Report History Record overview with filters applied and status badges.</div>---

</div>### What Gets Stored

A `ReportHistoryRecord` is created at render time when `createHistoryRecord: true` is set in the request, or automatically when a print task is dispatched. It captures a complete snapshot of the rendering event:

<div id="bkmrk-field-description-tr" style="text-align: justify;"><table style="width: 100%; border-collapse: collapse;"><thead><tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;"><th style="text-align: left; padding: 6px 10px; white-space: nowrap; width: 23.7501%;">Field</th><th style="text-align: left; padding: 6px 10px; width: 76.3691%;">Description</th></tr></thead><tbody><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`traceId`</td><td style="padding: 6px 10px; width: 76.3691%;">Unique identifier shared across the render request, the history record, and any linked print task. Used to correlate events in logs and across systems.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`reportConfig`</td><td style="padding: 6px 10px; width: 76.3691%;">Reference to the `ReportConfig` that was rendered.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`outputType`</td><td style="padding: 6px 10px; width: 76.3691%;">The output type used: `base64`, `url`, or `none`.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`apiPayload`</td><td style="padding: 6px 10px; width: 76.3691%;">The complete render request body: parameters, data, flags, everything sent to the render endpoint. Stored as JSON.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`apiResponse`</td><td style="padding: 6px 10px; width: 76.3691%;">The complete API response returned by VeloxFactory, including any errors and, unless the render was laconic, the input it actually worked with. Stored as JSON.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`reportPdf`</td><td style="padding: 6px 10px; width: 76.3691%;">The rendered PDF, Base64-encoded. Present on successful renders; `null` on failure.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`reportPdfFileName`</td><td style="padding: 6px 10px; width: 76.3691%;">The UUID-based filename assigned to the rendered PDF.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`reportExcelFileName`</td><td style="padding: 6px 10px; width: 76.3691%;">Filename of the xlsx export, present when a mailing asked for one. The file hangs on the record, is downloadable from it and is deleted with it.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`reportThumbnail`</td><td style="padding: 6px 10px; width: 76.3691%;">A thumbnail image of the first page of the rendered PDF. Generated asynchronously in the background after the record is created.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`status`</td><td style="padding: 6px 10px; width: 76.3691%;">Automatically calculated from the stored response. See below.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`reportPrintTasks`</td><td style="padding: 6px 10px; width: 76.3691%;">Every print job dispatched from this record, with printer, copies and status. A record can hold any number of them.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`reportMailTasks`</td><td style="padding: 6px 10px; width: 76.3691%;">Every mail sent from this record, with recipients, subject, the rendered body and the attachment names.</td></tr><tr><td style="padding: 6px 10px; white-space: nowrap; width: 23.7501%;">`audit`</td><td style="padding: 6px 10px; width: 76.3691%;">Who created the record and when, who last updated it, and which API token was used in either case. VeloxFactory derives `creationMethod` and `updateMethod` from it, `Frontend` or `API`, depending on whether a token was present on the request.</td></tr></tbody></table>

</div>The audit block is what turns a history record into an answer to "who printed this". A render triggered by an integration carries the token it was made with, a render triggered in the browser carries the user.

#### How It Comes Back from the API

The record is stored in full, but a response only carries what you ask for. Four query parameters control this, and they matter, a history record with an embedded PDF is a large object:

<div id="bkmrk-parameter-default-ef" style="text-align: justify;"><table style="width: 100%;"><thead><tr><th style="width: 17.64%;">Parameter</th><th style="width: 9.65492%;">Default</th><th style="width: 72.705%;">Effect</th></tr></thead><tbody><tr><td style="width: 17.64%;">`withApiPayloads`</td><td style="width: 9.65492%;">`true`</td><td style="width: 72.705%;">Includes `apiPayloadBase64` and `apiResponseBase64`. Both are Base64-encoded, so the stored JSON survives transport unchanged.</td></tr><tr><td style="width: 17.64%;">`withMedia`</td><td style="width: 9.65492%;">`false`</td><td style="width: 72.705%;">Adds the `media` block with `reportPdfBase64`, `reportPdfFileName` and `thumbnailBase64`.</td></tr><tr><td style="width: 17.64%;">`withRelations`</td><td style="width: 9.65492%;">`false`</td><td style="width: 72.705%;">Adds the full `reportConfig`, plus `reportPrintTasks` and `reportMailTasks`.</td></tr><tr><td style="width: 17.64%;">`withAudit`</td><td style="width: 9.65492%;">`false`</td><td style="width: 72.705%;">Adds the `audit` block described above.</td></tr></tbody></table>

</div>Without any of them a record comes back as a compact summary: ID, trace ID, output type, status and the payloads. That is the right shape for a list view, and it is what the frontend's history overview uses.

<div id="bkmrk-" style="text-align: justify;">---

</div>### Status

The status of a `ReportHistoryRecord` is calculated automatically every time the record is saved, based on the content of the stored API response:

<div id="bkmrk-status-meaning-ok-no" style="text-align: justify;"><table style="width: 100%; border-collapse: collapse;"><thead><tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;"><th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Status</th><th style="text-align: left; padding: 6px 10px;">Meaning</th></tr></thead><tbody><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><span style="color: #349b31; font-weight: 600;">Ok</span></td><td style="padding: 6px 10px;">No errors in the response and a PDF was produced. The render completed successfully.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><span style="color: #c0392b; font-weight: 600;">Error</span></td><td style="padding: 6px 10px;">The response contains one or more errors. The render failed, the error messages are stored in the API response payload.</td></tr><tr style="border-bottom: 1px solid #e6e8ef;"><td style="padding: 6px 10px;"><span style="color: #203671; font-weight: 600;">Render Fail</span></td><td style="padding: 6px 10px;">No errors in the response, but no PDF was produced either. An edge case indicating something unexpected occurred during rendering.</td></tr><tr><td style="padding: 6px 10px;"><span style="color: #525e5a; font-weight: 600;">Unknown</span></td><td style="padding: 6px 10px;">Status could not be determined from the stored response.</td></tr></tbody></table>

</div>History records are created for both successful and failed renders. A failed render still produces a complete record, including the error messages, which is often more useful than a successful one when something goes wrong.

<div id="bkmrk--1" style="text-align: justify;">---

</div>### The Thumbnail

When a history record is created, VeloxFactory dispatches a background job that converts the first page of the rendered PDF into a thumbnail image using `pdftoppm` (part of `poppler-utils`). The thumbnail is stored on the record and displayed in the history list and the report card grid, giving you an immediate visual of what was produced without opening the PDF.

<div id="bkmrk-%E2%84%B9%EF%B8%8F-thumbnail-generat" style="text-align: justify;"><div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">ℹ️ **Thumbnail generation runs asynchronously.** The history record is available immediately after rendering; the thumbnail appears once the background job has completed. This requires the Laravel queue worker (Supervisor) to be running. If the queue is down, thumbnails will not be generated until it is back up.</div>---

</div>### Traceability and Debugging

The most valuable aspect of a history record is not the PDF, it is the payload. Every record stores the exact request that triggered the render and the exact response that came back. This means you can answer the following questions at any point in the future, without touching the calling application:

<div id="bkmrk-what-parameters-were" style="text-align: justify;">- What parameters were passed to this render?
- What data was submitted, and which rows the report was actually filled with?
- Was the render triggered via the frontend or the API?
- What did VeloxFactory return, and did it succeed?
- If it failed, what was the exact error message?

</div>The `traceId` ties everything together. It is present on the history record, on any linked print task, and in the server logs. When something goes wrong in production and you have a `traceId`, you can pull the history record and reconstruct the entire event in seconds.

<div id="bkmrk-screenshot-placehold-1" style="text-align: justify;"><div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525e5a;">**Screenshot placeholder:** `report-history-record.show.png`  
Report History Record detail page with status, payload, PDF preview and the actions.</div>---

</div>### Reprinting from a History Record

A successful history record holds the rendered PDF. That PDF can be dispatched to a printer at any time, without re-rendering the report, using the dedicated print endpoint:

```
POST /api/v1/report-history-record/{id}/print

```

```json
{
  "printerName": "WarehousePrinter01",
  "numberOfCopies": 1
}

```

VeloxFactory creates a new `ReportPrintTask` from the stored PDF, assigns it a derived trace ID (the original trace ID with a short random suffix), and dispatches it to the print service. The original history record is linked to the new print task.

This is useful in several scenarios: a print job failed and needs to be retried, a physical document was lost and needs to be reprinted, or a record needs to be dispatched to a different printer than the one originally used.

<div id="bkmrk-%E2%84%B9%EF%B8%8F-reprinting-uses-t" style="text-align: justify;"><div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">ℹ️ **Reprinting uses the stored PDF, it does not re-render the report.** The document produced is identical to the original. If the report template or its data has changed since the original render, those changes are not reflected in the reprint.</div></div>This endpoint is also available directly from the frontend, the history record detail view has a **Print** button that opens a modal to enter the printer name and number of copies.

<div id="bkmrk-screenshot-placehold-2" style="text-align: justify;"><div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525e5a;">**Screenshot placeholder:** `report-history-record.print.modal.png`  
Print dialog of a Report History Record with the printer picker and the copies field.</div>---

</div>### Mailing from a History Record

The same idea applies to mail. A stored rendering can be sent to anyone at any time, without re-rendering:

```
POST /api/v1/report-history-record/{id}/mail

```

```json
{
  "mailer": "Office SMTP",
  "mailTemplate": "Report Delivery",
  "to": ["disposition@example.com"],
  "includePdf": true,
  "includeExcel": false,
  "sendAsync": false
}

```

VeloxFactory creates a new `ReportMailTask` from the stored PDF, gives it a derived trace ID, and sends the mail through the chosen mailer. Recipients, contact name and attachment names accept the same placeholders as a mailing in the render request, resolved against the parameters and rows stored on the record.

If `includeExcel` is set and the record does not have an xlsx yet, VeloxFactory builds one on the fly and attaches it to the record. The export uses the rows the report was actually filled with, which includes the rows an SQL adapter fetched at render time: they travel back in the render response and are stored on the record, so a report that gets its data from a live connection exports just as well as one whose rows came in the request.

<div id="bkmrk-%E2%9A%A0%EF%B8%8F-one-case-has-noth" style="text-align: justify;"><div style="border-left: 4px solid #f9c846; background: #fffbf0; padding: 12px 16px; margin: 16px 0; color: #525e5a;">⚠️ **One case has nothing to export.** A render made with `laconicResponse: true` stores a response without its input segment. If that request carried no `data` of its own either, the record holds no rows, and a later export answers with a clear error instead of an empty sheet. Render with the mailing segment, or without `laconicResponse`, when an xlsx may be wanted afterwards.</div></div>In the frontend the detail view has a **Mail** button next to **Print**, opening a dialog with mailer, template, recipients, attachments and the background switch. Every mail sent from this record is listed on the record itself, and in the Mail Queue.

<div id="bkmrk-screenshot-placehold-3" style="text-align: justify;"><div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525e5a;">**Screenshot placeholder:** `mailing.history-record-modal.png`  
Mail dialog of a Report History Record with mailer, template, recipients, attachments and the background switch.</div><div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">ℹ️ **Mailing uses the stored PDF too.** The document that arrives is identical to the original rendering. The full reference is on [Report Mailing](https://docs.veloxfactory.kiwi-software.dev/books/veloxfactory/page/report-mailing-sending-renderings-by-mail).</div>---

</div>### Retention and Deletion

#### Automatic Purging

History records are cleaned up by the **Purge Job**, the consolidated cleanup schedule of the Job Scheduler. How long a record is kept is a retention on that schedule, editable in the frontend or through the API; an empty value switches the step off entirely. The `PURGE_HISTORY_DAYS` environment variable supplies the value the schedule is created with, nothing more.

#### Deletion Constraints

A `ReportHistoryRecord` cannot be deleted while any `ReportPrintTask` or `ReportMailTask` still references it. The linked tasks must be removed first. VeloxFactory provides a dedicated endpoint for the print tasks:

```
DELETE /api/v1/report-history-record/{id}/delete-all-report-print-tasks

```

This removes all print tasks associated with the record in one call. Mail tasks have their own retention on the Purge Job, which runs before the history records for exactly this reason, and they can be deleted individually from the Mail Queue. Once no task references the record any more, the history record itself can be deleted, which also removes its PDF and its xlsx from disk.

#### Impact on ReportConfig Deletion

A `ReportConfig` cannot be deleted while any history records reference it. This is a deliberate constraint: the history exists as a permanent trace of what was rendered using that configuration. To remove a report configuration entirely, its history records and their linked print tasks must be cleared first, either manually or by waiting for the automatic purge to run.

# Scan2Print: Printing labels by scanning

<div style="text-align: justify;">

**Scan2Print** is a dedicated, mobile-optimized page for printing labels and documents straight from a barcode scan. It is built for handheld terminals (MDE) and smartphones, but works just as well on a desktop workstation with a USB scanner. Configure the station once, then every scan becomes a print job, no forms, no clicks, no PDF preview in between.

Typical use cases are inbound goods labels, article and batch labels, inspection tags or pallet labels, anywhere a worker scans a value and needs a printed label a second later.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scan2print.index-mobile.png</code><br>Scan2Print page on a handheld (approx. 360 px width): top bar with configuration summary and gear button, two scan fields, Print button.
</div>

---

<h3 id="how-it-works" style="color: #203671; margin-top: 2.2em;">How It Works</h3>

1. Open **Scan2Print** from the main navigation.
2. On the first visit, a configuration dialog opens. Select the report, the printer, the number of copies and fill in the fixed values for this station.
3. The page now shows one input field per scan parameter of the report, stacked on top of each other.
4. Scan the values. Every scan ends with **Enter**, which moves the cursor to the next field. The Enter on the last field triggers the render.
5. VeloxFactory renders the report, creates a print task and returns to the empty scan form. The cursor is back in the first field, ready for the next scan.

Under the hood, Scan2Print uses the same rendering pipeline as the [Generate PDF page](https://docs.veloxfactory.kiwi-software.dev/books/veloxfactory/page/rendering-from-the-frontend) and the API. Every Scan2Print submit is a regular render request with a print task attached, so everything you know about print tasks, history records and SQL data adapters applies here as well.

---

<h3 id="permissions" style="color: #203671; margin-top: 2.2em;">Permissions</h3>

Scan2Print has its own permission, found in the user settings under the section **Scan2Print**:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Permission</th>
      <th style="text-align: left; padding: 6px 10px;">Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>scan2print:use</code></td>
      <td style="padding: 6px 10px;">Shows the Scan2Print navigation item and grants access to the page. Administrators (<code>global:admin</code>) have access automatically.</td>
    </tr>
  </tbody>
</table>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scan2print.user-permission.png</code><br>User edit page, section <em>Scan2Print</em> with the <em>Use</em> checkbox.
</div>

`scan2print:use` is all a scan user needs. Every Scan2Print submit runs through the regular rendering pipeline, which checks its own permissions for rendering, print tasks and history records. The Scan2Print permission therefore implicitly includes these permissions, there is no need to grant them separately:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Implicit permission</th>
      <th style="text-align: left; padding: 6px 10px;">Required for</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Report Configs: <strong>Read</strong></td>
      <td style="padding: 6px 10px;">Selecting a report and rendering it.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Report Print Tasks: <strong>Read</strong> and <strong>Create</strong></td>
      <td style="padding: 6px 10px;">Creating the print job after every scan and checking the print queue.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Report History Records: <strong>Create</strong> and <strong>Update</strong></td>
      <td style="padding: 6px 10px;">Storing the render in the report history, if enabled in the station configuration.</td>
    </tr>
  </tbody>
</table>

As a consequence, a Scan2Print user also sees the **Reports** and **Print Queue** items in the navigation and can open report configurations read-only, including their Generate PDF page.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Dedicated scan users land on Scan2Print directly.</strong> Users who hold <code>scan2print:use</code> but may not create or edit report configurations are redirected to Scan2Print when they open the application root. Bookmarking the Scan2Print page on the device works as well, after a login VeloxFactory returns to it automatically.
</div>

---

<h3 id="preparing" style="color: #203671; margin-top: 2.2em;">Preparing a Report for Scan2Print</h3>

Scan2Print does not need a special report type. It works with any report configuration and decides by **parameter name prefix** which values are scanned and which are fixed:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Prefix</th>
      <th style="text-align: left; padding: 6px 10px;">Role</th>
      <th style="text-align: left; padding: 6px 10px;">Filled in</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>P_SCAN_</code></td>
      <td style="padding: 6px 10px;">Scan field. Appears as an input on the scan page. Always mandatory.</td>
      <td style="padding: 6px 10px;">On every scan</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>P_STATIC_</code></td>
      <td style="padding: 6px 10px;">Fixed value for the station, e.g. client name, storage location or inspector.</td>
      <td style="padding: 6px 10px;">Once, in the configuration dialog</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>P_RESOURCE_</code></td>
      <td style="padding: 6px 10px;">Image resource, handled automatically as usual.</td>
      <td style="padding: 6px 10px;">Never</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">any other</td>
      <td style="padding: 6px 10px;">Regular parameter. Not filled by Scan2Print, but can be delivered by the SQL query (see below).</td>
      <td style="padding: 6px 10px;">Never</td>
    </tr>
  </tbody>
</table>

The scan fields appear in the order in which the parameters are declared in the `.jrxml`. The field label is derived from the parameter name, `P_SCAN_ARTICLE_NUMBER` is shown as **Article Number**. Scan and static fields use the same typed inputs as the Generate PDF page, a `java.sql.Date` static parameter becomes a date picker, a `java.lang.Integer` scan field a numeric input.

A minimal parameter block looks like this:

```xml
<parameter name="P_SCAN_ARTICLE_NUMBER" class="java.lang.String">
    <property name="exampleValue" value="56932.2"/>
    <property name="required" value="true"/>
</parameter>
<parameter name="P_SCAN_ARTICLE_BATCH" class="java.lang.String">
    <property name="exampleValue" value="B69854/2026"/>
    <property name="required" value="true"/>
</parameter>
<parameter name="P_STATIC_INBOUND_DATE" class="java.lang.String">
    <property name="exampleValue" value="2026-05-02"/>
    <property name="required" value="true"/>
</parameter>
```

<h4 style="color: #203671; margin-top: 1.4em;">Which reports can be selected</h4>

Only reports that Scan2Print can actually fill are offered in the configuration dialog. A report is selectable if **all** of the following apply:

- It has at least one `P_SCAN_` parameter.
- None of its `P_SCAN_` parameters is of type `java.lang.Boolean`, a toggle cannot be scanned.
- It has **no detail band**, or it has an **SQL data adapter**. Scan2Print never sends report lines, so detail data can only come from a query.
- It has no **required** parameter outside `P_SCAN_` and `P_STATIC_`, because Scan2Print has no way to fill it.

If your report does not show up in the dropdown, check it against this list first.

<h4 style="color: #203671; margin-top: 1.4em;">Enriching the label with SQL</h4>

The real strength of Scan2Print comes with an SQL data adapter. Scan only the key, for example an article number, and let the query fetch everything else. Scan values are available in the query like any other parameter, and every column aliased with a parameter name is transferred into that parameter:

```sql
select
    a.description as 'P_ARTICLE_DESCRIPTION'
  , b.barcode     as 'P_ARTICLE_BARCODE'
  , a.supplier    as 'P_ARTICLE_SUPPLIER'
from article a
inner join articlebarcode b on a.id = b.articleId
where a.articleNumber = :P_SCAN_ARTICLE_NUMBER
```

Parameters that are filled by the query must be marked as **not required** in the `.jrxml`. VeloxFactory validates required parameters before the query runs, so a required parameter that only the query delivers would always fail.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>A query must return at least one row.</strong> If a report has an SQL data adapter and the query finds nothing, for example because an unknown article number was scanned, the render fails with <em>"No data delivered (or fetched via SQL using parameters)..."</em>. No print task is created, so no half-empty label is printed. If the query returns several rows, the values of the last row are used for the parameters.
</div>

---

<h3 id="configuration" style="color: #203671; margin-top: 2.2em;">Configuring the Scan Station</h3>

The configuration dialog opens automatically when no configuration exists yet. Afterwards, it can be reopened at any time with the **gear** button in the top bar of the page, which also shows a compact summary of the active configuration (report, printer, copies, broadcast ID and static values).

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Option</th>
      <th style="text-align: left; padding: 6px 10px;">Default</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Report</td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">Searchable list of all Scan2Print capable reports. Required.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Printer Name</td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">The target printer for every print task of this station. Required.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Copies</td>
      <td style="padding: 6px 10px;">1</td>
      <td style="padding: 6px 10px;">Number of copies per scan, between 1 and 100.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Broadcast ID</td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">Optional WebSocket channel ID for real-time notification of the print service. Leave empty to rely on polling.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Create History Record</td>
      <td style="padding: 6px 10px;"><span style="color: #349b31; font-weight: 600;">On</span></td>
      <td style="padding: 6px 10px;">Stores every scan render in the report history, including request, response and PDF. Can be switched off for high-volume stations.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Static Parameters</td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">One typed input per <code>P_STATIC_</code> parameter of the selected report. They are loaded as soon as a report is selected and replaced when you switch to another report. All of them are required, except boolean toggles.</td>
    </tr>
  </tbody>
</table>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scan2print.config-modal.png</code><br>Configuration dialog with selected report, printer, copies, broadcast ID, history toggle and loaded static parameters.
</div>

Click **Save** to store the configuration. If something is missing or invalid, VeloxFactory shows an error message and reopens the dialog with your input still in place.

To reset the station, open the dialog and click **Clear configuration**. The scan form disappears and the dialog asks for a new configuration.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Static values are fixed.</strong> A static date stays exactly as entered, it does not move on to the next day. If a label should always show the current date, use a default expression in the <code>.jrxml</code> instead of a static parameter.
</div>

---

<h3 id="scanning" style="color: #203671; margin-top: 2.2em;">Scanning and Printing</h3>

With a valid configuration, the page shows all scan fields of the report, large and full width, with the cursor already in the first field.

- **Enter** in a filled field moves the cursor to the next field and selects its content.
- **Enter** in the last field submits the form and starts the render.
- **Enter** in an empty field does nothing, the cursor stays where it is. All scan fields are mandatory.
- The **Print** button below the fields submits the form as well, which is handy for manual input on a desktop.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scan2print.scan-form.png</code><br>Scan form on desktop with a configured report, cursor in the first scan field.
</div>

After submitting, the form is locked until the page has reloaded. Scanners that send their data in quick bursts or add an extra Enter therefore never create duplicate print jobs. Reloading the page after a print does not print again either.

---

<h3 id="result" style="color: #203671; margin-top: 2.2em;">The Result</h3>

<h4 style="color: #203671; margin-top: 1.4em;">Success</h4>

A short green notification confirms the print job for two seconds, including report name, printer and number of copies. The scan fields are empty again and the cursor is back in the first field. The print task is now in the print queue and will be picked up by the print service.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scan2print.result-success.png</code><br>Green notification after a successful scan, empty scan fields.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Errors</h4>

If the render fails, a red block appears in the page body. It stays visible until the next scan and contains:

- all error messages returned by VeloxFactory, e.g. an unknown article number or a missing value,
- the values that were scanned, so the worker knows which item failed,
- a link to the print queue.

The scan fields are cleared and the cursor is back in the first field, so the worker can simply scan again.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scan2print.result-error.png</code><br>Red error block with error message (e.g. unknown article number), scanned values and link to the print queue.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Check the print queue before rescanning after a WebSocket error.</strong> If the broadcast to the print service fails, for example because Reverb is not running, the print task has already been created and is still printed by polling. Scan2Print shows an error in this case, a rescan would print the label twice.
</div>

---

<h3 id="session" style="color: #203671; margin-top: 2.2em;">Where the Configuration Is Stored</h3>

The station configuration is stored in the **user session** on the server. This keeps each device independent, two handhelds logged in with different users can serve different reports and printers at the same time.

- The configuration survives page reloads and navigation within VeloxFactory.
- It is removed on logout and when the session expires after the configured idle time (`SESSION_LIFETIME`). The worker then logs in again and reconfigures the station. After a session timeout, a pending scan cannot be submitted anymore and the page has to be reloaded.
- Static values are stored by parameter name. If the report is deleted, loses its Scan2Print capability, or a new `.jrxml` with additional required static parameters is uploaded, the configuration is reset automatically on the next page load, a warning is shown and the configuration dialog opens.

---

<h3 id="scanner" style="color: #203671; margin-top: 2.2em;">Scanner Requirements and Tips</h3>

- Scan2Print expects a scanner in **keyboard mode** (keyboard wedge), as used by nearly all handheld terminals and USB scanners.
- Configure the scanner to send **Enter** as suffix. A **Tab** suffix only moves the cursor and never submits the form.
- Camera scanning is not part of Scan2Print. Smartphones need a hardware scanner or a scanner app that types into the focused field.
- On smartphones, the on-screen keyboard opens when a field is focused. This is intended, it keeps manual input possible.
- For barcodes on the printed label, prefer **Code128** when the label should reproduce the scanned value exactly. EAN-13 components pad shorter values such as EAN-8 codes with zeros and recalculate the check digit, so the printed barcode would differ from the scanned one.

</div>

# Print your renderings

Getting a rendering onto paper: print tasks, printer master data and our own print service.

# Creating Report Print Tasks

<div style="text-align: justify;">

A `ReportPrintTask` represents a single print job, a PDF queued for delivery to a physical printer. VeloxFactory does not communicate with printers directly. Instead, it creates the task record, optionally notifies a separate print service via WebSocket, and waits for the service to report back. This page covers how tasks are created, how the status lifecycle works, and how to handle retries. Its counterpart for the other delivery route is the `ReportMailTask`, described on <a href="/books/veloxfactory/page/report-mailing-sending-renderings-by-mail">Report Mailing</a>: same idea, same three ways to be created, a mailbox instead of a printer.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>report-print-task.index.png</code><br>Report Print Task overview with status badges, printer and the reset action.
</div>

---

<h3 id="three-ways" style="color: #203671; margin-top: 2.2em;">Three Ways to Create a Print Task</h3>

<h4 style="color: #203671; margin-top: 1.4em;">1. As Part of a Render Request</h4>

The most common path: set `createPrintTask: true` in the render request body, provide a `printerName`, and VeloxFactory renders the report and dispatches it to the printer in a single call. No second request needed.

```json
POST /api/v1/report-config/A5_KanBan/render

{
  "outputType": "base64",
  "parameters": { "P_ARTICLE_NUMBER": "4561287-154" },
  "data": [ { ... } ],
  "createHistoryRecord": true,
  "createPrintTask": true,
  "printerName": "WarehousePrinter01",
  "numberOfCopies": 1,
  "broadcastId": "Standard"
}
```

<h4 style="color: #203671; margin-top: 1.4em;">2. From a History Record</h4>

A task can be dispatched from any existing `ReportHistoryRecord`, without re-rendering the report. VeloxFactory uses the PDF stored in the history record and creates a new print task from it:

```
POST /api/v1/report-history-record/{id}/print
```

```json
{
  "printerName": "WarehousePrinter01",
  "numberOfCopies": 2
}
```

This is the standard reprint path. See [The concept of Report History Records](#) for details.

<h4 style="color: #203671; margin-top: 1.4em;">3. Standalone via the Print Task API</h4>

Print tasks can also be created directly, independently of any render or history record. The `POST /api/v1/report-print-task` endpoint accepts any PDF as a Base64 string, making it possible to use the VeloxFactory print infrastructure for documents that were not produced by VeloxFactory at all.

```json
POST /api/v1/report-print-task

{
  "fileName":       "delivery_note_5521.pdf",
  "fileBase64":     "JVBERi0xLjQ...",
  "printerName":    "WarehousePrinter01",
  "numberOfCopies": 1,
  "broadcastId":    "Standard"
}
```

`reportConfig` and `reportHistoryRecord` are both optional on this endpoint, the task is created without either relation if they are not provided.

---

<h3 id="data-model" style="color: #203671; margin-top: 2.2em;">The Data Model</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Field</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>traceId</code></td>
      <td style="padding: 6px 10px;">Unique identifier. Shared with the linked history record when the task was created via a render request. For reprints, a derived trace ID is generated (original + short random suffix).</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>reportConfig</code></td>
      <td style="padding: 6px 10px;">The <code>ReportConfig</code> the printed PDF was generated from. Optional, not present for standalone tasks.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>reportHistoryRecord</code></td>
      <td style="padding: 6px 10px;">The linked <code>ReportHistoryRecord</code>. Optional, not present for standalone tasks.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>printerName</code></td>
      <td style="padding: 6px 10px;">The name of the target printer, as the print service expects it.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>numberOfCopies</code></td>
      <td style="padding: 6px 10px;">Number of copies passed to the print service. VeloxFactory always renders once, the print service is responsible for duplication. Defaults to <code>1</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>broadcastId</code></td>
      <td style="padding: 6px 10px;">WebSocket channel ID. If set at creation time, VeloxFactory broadcasts a <code>ReportPrintTaskCreated</code> event via Laravel Reverb. Omit to use polling instead.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>outputFileName</code></td>
      <td style="padding: 6px 10px;">The filename of the PDF queued for printing.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>status</code></td>
      <td style="padding: 6px 10px;">Current state of the task. See below.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>errorMessage</code></td>
      <td style="padding: 6px 10px;">Failure detail reported by the print service. <code>null</code> unless status is <code>error</code>.</td>
    </tr>
  </tbody>
</table>

---

<h3 id="status-lifecycle" style="color: #203671; margin-top: 2.2em;">Status Lifecycle</h3>

Every print task starts as `pending`. The print service picks it up, executes the job, and reports the result back to VeloxFactory via the API:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Status</th>
      <th style="text-align: left; padding: 6px 10px;">Set by</th>
      <th style="text-align: left; padding: 6px 10px;">Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;"><span style="color: #203671; font-weight: 600;">pending</span></td>
      <td style="padding: 6px 10px;">VeloxFactory</td>
      <td style="padding: 6px 10px;">Task created, waiting for the print service to pick it up.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;"><span style="color: #349b31; font-weight: 600;">printed</span></td>
      <td style="padding: 6px 10px;">Print service</td>
      <td style="padding: 6px 10px;">Print job executed and confirmed.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;"><span style="color: #c0392b; font-weight: 600;">error</span></td>
      <td style="padding: 6px 10px;">Print service</td>
      <td style="padding: 6px 10px;">Print job failed. <code>errorMessage</code> contains the failure detail.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;"><span style="color: #525E5A; font-weight: 600;">unknown</span></td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">Status could not be determined.</td>
    </tr>
  </tbody>
</table>

The print service reports back using the dedicated status endpoint:

```
PATCH /api/v1/report-print-task/{id}/set-status

{
  "status": "error",
  "errorMessage": "Printer offline"
}
```

<h4 style="color: #203671; margin-top: 1.4em;">Resetting to Pending</h4>

A task can be reset to `pending` using the set-printed shortcut endpoint, setting the status flag to `false`:

```
PATCH /api/v1/report-print-task/{id}/set-printed/false
```

This re-queues the task. If the task has a `broadcastId`, VeloxFactory re-broadcasts the `ReportPrintTaskCreated` event immediately, notifying the print service to pick the task up again without polling. This is the standard retry mechanism for failed or stalled print jobs.

---

<h3 id="websocket-vs-polling" style="color: #203671; margin-top: 2.2em;">WebSocket vs. Polling</h3>

How the print service learns about a new task depends on whether a `broadcastId` is set.

**With `broadcastId`**: VeloxFactory broadcasts a `ReportPrintTaskCreated` event via WebSocket (Laravel Reverb) the moment the task is created. The print service subscribes to the channel identified by `broadcastId` and reacts immediately. This is the recommended mode for real-time printing, the task reaches the printer within milliseconds of the render completing.

**Without `broadcastId`**: No broadcast is sent. The print service must poll `GET /api/v1/report-print-task?status=pending` at a regular interval and process any tasks it finds. This works fine for workflows where sub-second delivery is not required.

Each poll returns one page of the waiting tasks, oldest first: 25 by default, up to 100 with `perPage`. A poller that reports every task back via `set-status` can simply request the first page again, finished tasks drop out of the `pending` filter by themselves. `meta.pagination.total` tells how many tasks are still waiting, see [Pagination](/books/veloxfactory/page/meet-the-api#pagination).

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>WebSocket delivery requires Laravel Reverb to be running.</strong> If Reverb is down, task creation will fail with an error rather than falling back silently to polling. Use Supervisor to keep the Reverb process alive, the same Supervisor configuration that manages the Laravel queue worker should include a <code>php artisan reverb:start</code> program entry. See <a href="/books/veloxfactory/page/installing-veloxfactory">Installing VeloxFactory</a> for a reference configuration.
</div>

---

<h3 id="retention" style="color: #203671; margin-top: 2.2em;">Retention and Deletion</h3>

Print tasks are automatically purged after a configurable number of days, set via the `PURGE_PRINTTASKS_DAYS` environment variable (default: 30 days). Purging runs as a scheduled background job, no manual action required.

Individual tasks can also be deleted directly via the API at any time:

```
DELETE /api/v1/report-print-task/{id}
```

There are no deletion constraints on print tasks themselves, they can always be removed. However, deleting a print task is a prerequisite for deleting the linked `ReportHistoryRecord`, which in turn must be cleared before a `ReportConfig` can be deleted. Mail tasks block a history record in exactly the same way and have their own retention setting, `PURGE_MAILTASKS_DAYS`. Automatic purging handles the whole chain in the background once the retention periods expire.

</div>

# Our own C#-based print service

<div style="text-align: justify;">

VeloxFactory does not talk to printers directly. Instead, a lightweight companion application, the **Background Printing Service**, runs on any Windows machine that has the target printers installed. It receives print tasks from VeloxFactory, renders the PDF to the printer, and reports the result back. The two components communicate exclusively over the VeloxFactory API and WebSocket; there is no shared database or filesystem.

---

<h3 id="how-it-works" style="color: #203671; margin-top: 2.2em;">How it works</h3>

The service starts as a regular Windows console process and works through two sequential phases.

<h4 style="color: #203671; margin-top: 1.4em;">Phase 1 - Initial pull</h4>

On startup the service immediately calls `GET /api/v1/report-print-task?status=pending` and processes all tasks it finds. It repeats this in a loop, waiting two seconds between rounds, until the queue comes back empty *and* no jobs are still running. This ensures that any tasks queued while the service was offline are handled before switching to real-time mode.

<h4 style="color: #203671; margin-top: 1.4em;">Phase 2 - WebSocket listener</h4>

Once the initial queue is drained, the service connects to VeloxFactory's WebSocket endpoint (Laravel Reverb) and subscribes to the private channel `private-report-print-tasks`. From this point on, it reacts to incoming events in real time. If the WebSocket connection drops for any reason, the service waits five seconds and reconnects automatically, no manual restart required.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The WebSocket uses the Pusher protocol.</strong> When a connection is established, the service authenticates with VeloxFactory via <code>POST /api/v1/broadcasting/auth</code> and subscribes to the private channel using the configured API token.
</div>

---

<h3 id="processing" style="color: #203671; margin-top: 2.2em;">Processing a print task</h3>

Whether a task arrives via the initial pull or via a WebSocket event, the processing steps are identical:

1. **Fetch**: The service calls `GET /api/v1/report-print-task/{id}` to retrieve the full task record, including the PDF as a Base64 string.
2. **Write temp file**: The PDF is decoded and written to a temporary file in `reportPdfFileTempPath` (e.g. `C:\VeloxFactory\temp\42_delivery_note.pdf`).
3. **Print**: PdfiumViewer opens the PDF and sends it to the printer specified in `printerName`. The print is repeated `numberOfCopies` times.
4. **Report back**: On success, the service calls `PATCH /api/v1/report-print-task/{id}/set-printed`, which sets the status to `printed`. On failure, it calls `PATCH /api/v1/report-print-task/{id}/set-status` with `{"status": "error", "errorMessage": "..."}`.
5. **Cleanup**: The temporary file is deleted regardless of the outcome.

<div style="border-left: 4px solid #203671; background: #f0f3fb; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>The WebSocket event only carries the task ID and <code>broadcastId</code>, not the PDF.</strong> The service always fetches the full task from the API as a second step. This means the printer machine needs HTTP access to VeloxFactory, not just WebSocket access.
</div>

---

<h3 id="broadcast-id-filtering" style="color: #203671; margin-top: 2.2em;">Broadcast ID filtering</h3>

`listeningBroadcastIds` is a list of broadcast channel identifiers the service will accept. Any `report-print-task.created` event whose `broadcastId` is not in this list is silently ignored.

This makes it straightforward to run multiple service instances in parallel, for example one per location or printer group, each configured to respond only to its own `broadcastId`. The initial pull is not filtered this way: it always processes all pending tasks returned by the API, regardless of `broadcastId`.

---

<h3 id="configuration" style="color: #203671; margin-top: 2.2em;">Configuration</h3>

All settings live in `App.config` in the `applicationSettings` section. Edit the file in a text editor and restart the service for changes to take effect.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Setting</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
      <th style="text-align: left; padding: 6px 10px;">Example</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>apiToken</code></td>
      <td style="padding: 6px 10px;">Bearer token used for all API requests and for the WebSocket subscription. Must belong to a user with <code>global:use-api</code> and with <code>report-print-task:read</code>, <code>:update</code> and <code>:delete</code>. The <code>:read</code> permission is also what the private channel <code>report-print-tasks</code> requires, a token without it can call the API but never receives a print job over the WebSocket.</td>
      <td style="padding: 6px 10px;"><code>4|abc123...</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>websocketUrl</code></td>
      <td style="padding: 6px 10px;">WebSocket endpoint of Laravel Reverb.</td>
      <td style="padding: 6px 10px;"><code>ws://10.0.0.10:8080/app/veloxfactory</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>websocketAuthUrl</code></td>
      <td style="padding: 6px 10px;">VeloxFactory broadcasting auth endpoint.</td>
      <td style="padding: 6px 10px;"><code>http://10.0.0.10:8088/api/v1/broadcasting/auth</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>reportPrintTask_index</code></td>
      <td style="padding: 6px 10px;">URL for the initial pull, must include <code>?status=pending</code>.</td>
      <td style="padding: 6px 10px;"><code>http://10.0.0.10:8088/api/v1/report-print-task?status=pending</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>reportPrintTask_get</code></td>
      <td style="padding: 6px 10px;">URL template for fetching a single task. <code>{0}</code> is replaced with the task ID.</td>
      <td style="padding: 6px 10px;"><code>http://10.0.0.10:8088/api/v1/report-print-task/{0}</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>reportPrintTask_setPrinted</code></td>
      <td style="padding: 6px 10px;">URL template for marking a task as printed. <code>{0}</code> is replaced with the task ID.</td>
      <td style="padding: 6px 10px;"><code>http://10.0.0.10:8088/api/v1/report-print-task/{0}/set-printed</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>reportPrintTask_setError</code></td>
      <td style="padding: 6px 10px;">URL template for reporting a failed task. <code>{0}</code> is replaced with the task ID.</td>
      <td style="padding: 6px 10px;"><code>http://10.0.0.10:8088/api/v1/report-print-task/{0}/set-status</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>listeningBroadcastIds</code></td>
      <td style="padding: 6px 10px;">List of broadcast IDs this instance will accept. Add one <code>&lt;string&gt;</code> entry per ID.</td>
      <td style="padding: 6px 10px;"><code>Standard</code>, <code>Warehouse</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>maxParallelPrintJobs</code></td>
      <td style="padding: 6px 10px;">Maximum number of tasks processed concurrently. Default: <code>10</code>.</td>
      <td style="padding: 6px 10px;"><code>10</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>reportPdfFileTempPath</code></td>
      <td style="padding: 6px 10px;">Directory for temporary PDF files. Created automatically on startup if it does not exist.</td>
      <td style="padding: 6px 10px;"><code>C:\VeloxFactory\temp</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>logFile</code></td>
      <td style="padding: 6px 10px;">Path to the log file. Relative paths are resolved from the executable directory.</td>
      <td style="padding: 6px 10px;"><code>.\Log.log</code></td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>laconicLogging</code></td>
      <td style="padding: 6px 10px;">If <code>True</code>, only errors are logged. If <code>False</code>, all informational messages are logged as well.</td>
      <td style="padding: 6px 10px;"><code>False</code></td>
    </tr>
  </tbody>
</table>

---

<h3 id="concurrency" style="color: #203671; margin-top: 2.2em;">Concurrency</h3>

The service uses two layers of concurrency control to avoid overloading printers.

A global semaphore limits the total number of tasks being processed at the same time to `maxParallelPrintJobs`. In addition, a per-printer semaphore ensures that only one print job runs on a given printer at a time, jobs targeting different printers can execute in parallel, but two jobs targeting the same printer are always serialised. This prevents the spooler from receiving multiple jobs simultaneously from the service.

---

<h3 id="logging" style="color: #203671; margin-top: 2.2em;">Logging</h3>

The service uses **Serilog** and writes to both the console and a rolling log file. Log files are capped at 100 MB each; up to 10 rotated files are retained before the oldest is deleted.

Set `laconicLogging` to `True` in `App.config` to suppress informational messages and log only errors, useful in production once the service is confirmed working.

---

<h3 id="dependencies" style="color: #203671; margin-top: 2.2em;">Dependencies</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Package</th>
      <th style="text-align: left; padding: 6px 10px;">Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>PdfiumViewer</code></td>
      <td style="padding: 6px 10px;">PDF rendering and printing. Wraps the native PDFium library (bundled via <code>PdfiumViewer.Native.x86_64.v8-xfa</code>), no separate PDF reader installation required on the target machine.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>RestSharp</code></td>
      <td style="padding: 6px 10px;">HTTP client for all API calls to VeloxFactory.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>Newtonsoft.Json</code></td>
      <td style="padding: 6px 10px;">JSON serialisation and deserialisation (API responses, WebSocket messages).</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>Serilog</code></td>
      <td style="padding: 6px 10px;">Structured logging to console and rolling file.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The service targets .NET Framework 4.7.2 and runs on Windows only.</strong> The PDFium native binary is bundled with the build output, no additional runtime installation is needed beyond .NET Framework 4.7.2, which ships with Windows 10 and Windows Server 2016 and later.
</div>

</div>

# Printers: master data for your print targets

<div style="text-align: justify;">

A print task needs to know where it should come out. That destination is a queue name on the print server: exact, unforgiving and easy to mistype, and a typo is only noticed when nothing comes out of the printer. **Printers** turn that string into master data: a short list of the machines your company actually prints on, each with a readable name, a location and, if you use the WebSocket print service, its broadcast ID.

The print service itself sees none of that. VeloxFactory hands it the **queue name** of the printer and nothing else. The master data only decides which queue name is sent, and it lets everyone else work with names a human recognises.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>printer.index.png</code><br>Printer overview under Configuration with filter bar, type badges and the Active column.
</div>

---

<h3 id="where" style="color: #203671; margin-top: 2.2em;">Where to find it</h3>

The printer master data lives in the main menu under **Configuration → Printers**. The overview is a regular VeloxFactory lookup: search over display name, queue name, location and description, filters for type, active state, creator, updater and the creation date, and sortable columns. Like every lookup, the filter settings and the current page are kept in your session, so you come back to the list exactly as you left it.

**New** opens the create form, the pencil on a row opens an existing record. Deleting is done from the edit page.

---

<h3 id="fields" style="color: #203671; margin-top: 2.2em;">The fields of a printer</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Field</th>
      <th style="text-align: left; padding: 6px 10px;">Required</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Display Name</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">The name people use, for example <code>Warehouse Label 01</code>. Three to 50 characters and unique across all printers.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Printer Name</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">The queue name on the print server, for example <code>WH-LABEL-01</code>. This is the value the print service receives. Up to 50 characters and unique as well.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Printer Type</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">One of <code>label-printer</code>, <code>a4-printer</code>, <code>mfc-printer</code> or <code>digital-printer</code>. Purely descriptive, it drives the icon and the filter, not the rendering.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Location</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Where the machine stands, for example <code>Hall 2, Shipping</code>. Shown in the picker so a station is easy to identify.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Broadcast ID</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">The WebSocket channel of the print service instance that serves this printer. Set it once here and no one has to remember it again.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Description</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Free note, for example the label size or the media currently loaded.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Active</td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">On by default. Inactive printers stay in the master data and keep working in existing configurations, but they are not offered in the pickers.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Retiring a printer is a toggle, not a delete.</strong> Switch it to inactive and it disappears from every picker while the print tasks that reference it keep their history.
</div>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>printer.edit.png</code><br>Printer edit form with display name, queue name, type, location, broadcast ID, description and the Active toggle.
</div>

---

<h3 id="resolution" style="color: #203671; margin-top: 2.2em;">Name resolution</h3>

This is the part that makes the master data useful everywhere, including in scripts that know nothing about it. Whenever VeloxFactory receives a printer name, from the frontend, from the API or from an AI assistant, it resolves that name before the print task is stored:

1. The name is compared to the **queue name** of every printer record, ignoring upper and lower case.
2. If nothing matches, it is compared to the **display name**.
3. If a record matches, its queue name is stored on the print task, and its broadcast ID is filled in unless the caller sent one explicitly.
4. If nothing matches, the name is stored exactly as supplied.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">You send</th>
      <th style="text-align: left; padding: 6px 10px;">Stored printer name</th>
      <th style="text-align: left; padding: 6px 10px;">Stored broadcast ID</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>WH-LABEL-01</code></td>
      <td style="padding: 6px 10px;"><code>WH-LABEL-01</code></td>
      <td style="padding: 6px 10px;">from the record, if it has one</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>Warehouse Label 01</code></td>
      <td style="padding: 6px 10px;"><code>WH-LABEL-01</code></td>
      <td style="padding: 6px 10px;">from the record, if it has one</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>warehouse label 01</code></td>
      <td style="padding: 6px 10px;"><code>WH-LABEL-01</code></td>
      <td style="padding: 6px 10px;">from the record, if it has one</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>SOME-OTHER-QUEUE</code></td>
      <td style="padding: 6px 10px;"><code>SOME-OTHER-QUEUE</code></td>
      <td style="padding: 6px 10px;">only what you sent</td>
    </tr>
  </tbody>
</table>

Resolution runs on every path that accepts a printer name: rendering a report configuration with **Create Print Task**, creating or updating a print task directly, printing a stored history record again, and every Scan2Print scan. A broadcast ID you send yourself always wins over the one stored on the record.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>A plain queue name is always accepted.</strong> An integration that sends the queue name gets exactly the printer it asked for, with the broadcast ID filled in from the record if the printer has one. Nothing has to know that the master data exists.
</div>

---

<h3 id="picker" style="color: #203671; margin-top: 2.2em;">The printer picker</h3>

Wherever a printer is chosen, VeloxFactory shows the same control: a searchable dropdown of all **active** printers above a plain text field. The dropdown entries read `Display Name (queue name) - location`, and picking one writes the queue name into the text field and fills a neighbouring **Broadcast ID** field if the printer has one. The text field stays editable, so a printer that is not in the master data can still be addressed by typing its name.

The picker appears in four places:

- the **Output Settings** of the Generate PDF page
- the **Configuration** dialog of Scan2Print
- the edit dialog of a **Report Print Task**
- the print dialog of a **Report History Record**

If no printers are configured at all, the picker silently falls back to a plain text field with the note *No printers configured yet*. The master data is optional: an instance without a single printer record addresses its printers by typing the queue name.

---

<h3 id="printtasks" style="color: #203671; margin-top: 2.2em;">Print tasks and the printer link</h3>

Every print task keeps its printer name as text, that text is what the print service consumes. In addition, a print task whose name resolved to a master data record stores a reference to that record. This link is used for one thing only: the print queue can be filtered by printer, including the entry *Not linked to a printer* for everything that was addressed by free text.

Deleting a printer record therefore never touches history. The print tasks keep their stored printer name, they only lose the link, and the filter moves them to *Not linked to a printer*.

---

<h3 id="api" style="color: #203671; margin-top: 2.2em;">The API</h3>

Printers are a regular API resource under `/api/v1/printer`, with the same envelope, the same audit segment and the same error format as every other resource.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Endpoint</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /printer</code></td>
      <td style="padding: 6px 10px;">List printers page by page with <code>page</code> and <code>perPage</code> (see <a href="/books/veloxfactory/page/meet-the-api#pagination">Pagination</a>), optionally narrowed with <code>isActive</code> and <code>printerType</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /printer/{id}</code></td>
      <td style="padding: 6px 10px;">A single printer, with <code>withAudit=true</code> including its audit segment.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /printer</code></td>
      <td style="padding: 6px 10px;">Create a printer. <code>displayName</code>, <code>printerName</code> and <code>printerType</code> are required, both names must be unique.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>PATCH /printer/{id}</code></td>
      <td style="padding: 6px 10px;">Partial update: fields you do not send keep their stored value.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>DELETE /printer/{id}</code></td>
      <td style="padding: 6px 10px;">Delete a printer. Existing print tasks keep their stored printer name.</td>
    </tr>
  </tbody>
</table>

A printer resource is returned as:

```json
{
    "model": "Printer",
    "id": 1,
    "displayName": "Warehouse Label 01",
    "location": "Hall 2, Shipping",
    "printerName": "WH-LABEL-01",
    "printerType": "label-printer",
    "printerTypeLabel": "Label Printer",
    "broadcastId": "Standard",
    "isActive": true,
    "description": "4x6\" thermal labels"
}
```

---

<h3 id="mcp" style="color: #203671; margin-top: 2.2em;">AI assistants</h3>

The VeloxFactory MCP server exposes the same five operations as tools: `list_printers`, `get_printer`, `create_printer`, `update_printer` and `delete_printer`. Because name resolution happens inside VeloxFactory, an assistant can simply pass the name a person said. *Print this on the warehouse label printer* is enough, no lookup up front.

---

<h3 id="permissions" style="color: #203671; margin-top: 2.2em;">Permissions</h3>

Printers use the standard permission scheme. In the user editor they have their own **Printers** section.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Permission</th>
      <th style="text-align: left; padding: 6px 10px;">Grants</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>printer:read</code></td>
      <td style="padding: 6px 10px;">See the menu entry, the overview and a printer's detail page.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>printer:create</code></td>
      <td style="padding: 6px 10px;">Create printers.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>printer:update</code></td>
      <td style="padding: 6px 10px;">Change printers, including the active state.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>printer:delete</code></td>
      <td style="padding: 6px 10px;">Delete printers.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>printer:full</code></td>
      <td style="padding: 6px 10px;">All of the above, like <code>global:admin</code>.</td>
    </tr>
  </tbody>
</table>

The picker itself needs no printer permission. A user who may only scan labels still sees the configured printers in the Scan2Print dialog, they just cannot open or change the master data.

</div>

# Mail your renderings

Sending rendered reports by mail: the mailing segment, mailers, mail templates and the mail queue.

# Report Mailing: sending renderings by mail

<div style="text-align: justify;">

A rendered report usually has somewhere to go: back to the caller as Base64 or as a URL, straight into a print task, or into the mailbox of whoever needs the document. **Report Mailing** is that last destination. A render request carries an optional `mailing` segment, and once the render succeeded VeloxFactory sends an HTML mail through a configured mailer, using a configured template, with the PDF attached and, if you want it, an xlsx export of the same data.

Mailing is an addition to a render, never a condition of it. A request without a `mailing` segment is a plain render, and a mail that fails never turns a successful render into an error.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mailing.history-record-modal.png</code><br>The mail dialog of a Report History Record with mailer, template, recipients, attachments and the background switch.
</div>

---

<h3 id="parts" style="color: #203671; margin-top: 2.2em;">The three parts</h3>

Mailing is built from three things you configure once and one thing you send per render.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Part</th>
      <th style="text-align: left; padding: 6px 10px;">What it holds</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Mailer</strong></td>
      <td style="padding: 6px 10px;">The SMTP account: host, port, credentials, sender address, and the dispatch rate limits your provider allows. Master data under Configuration.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Mail Template</strong></td>
      <td style="padding: 6px 10px;">Subject and body, written in a WYSIWYG editor, with placeholders and optional whitelabeling. Master data under Configuration.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Mailing segment</strong></td>
      <td style="padding: 6px 10px;">Per request: which mailer, which template, who receives it, what is attached, and whether it goes out immediately or through the queue.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Report Mail Task</strong></td>
      <td style="padding: 6px 10px;">The record of one send attempt: recipients, the rendered subject and body, the attachments, the status and, if something went wrong, the error.</td>
    </tr>
  </tbody>
</table>

---

<h3 id="request" style="color: #203671; margin-top: 2.2em;">Mailing from a render request</h3>

The `mailing` segment is optional and sits next to the fields you already know:

```json
{
    "outputType": "base64",
    "parameters": { "P_ORDER_NO": "4711" },
    "createPrintTask": false,
    "createHistoryRecord": true,
    "mailing": {
        "mailer": "Office SMTP",
        "mailTemplate": "Report Delivery",
        "to": ["disposition@example.com", "[data.first.customerEmail]"],
        "cc": [],
        "bcc": ["archive@example.com"],
        "contactName": "[data.first.customerName]",
        "includePdf": true,
        "pdfFileName": "Order_[parameters.P_ORDER_NO]",
        "includeExcel": true,
        "includeParameters": true,
        "excelFileName": "Data_[traceId]",
        "sendAsync": false
    }
}
```

`mailer` and `mailTemplate` accept either the numeric ID or the unique name of the record, so a request stays readable. Only active records are resolved.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Field</th>
      <th style="text-align: left; padding: 6px 10px;">Required</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">mailer</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">ID or unique name of an active Mailer.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">mailTemplate</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">ID or unique name of an active Mail Template.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">to, cc, bcc</td>
      <td style="padding: 6px 10px;">to: yes</td>
      <td style="padding: 6px 10px;">Address lists. Literal addresses are validated before the render is spent, addresses containing a placeholder are resolved afterwards.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">contactName</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Free text for a personal greeting, available in the template as <code>[contactName]</code>. May itself be a placeholder.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">includePdf</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Default <code>true</code>. Set it to <code>false</code> for a notification mail that only carries the xlsx, or nothing at all.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">pdfFileName</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Base name the PDF carries as an attachment of the mail, without extension. The stored file keeps its own name, nothing is copied.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">includeExcel</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Default <code>false</code>. Builds an xlsx from the rows that were actually rendered.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">includeParameters</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Prints the parameters as a block above the data table in the xlsx.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">excelFileName</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Base name the xlsx carries as an attachment, same rules as <code>pdfFileName</code>.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">sendAsync</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Default <code>false</code>. <code>true</code> hands the mail to the queue and answers immediately.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>A failed mail never fails the render.</strong> Whatever goes wrong with the mailing, the render response stays a success and the detail lands in <code>meta.mailing</code> and on the mail task. Your integration keeps its PDF.
</div>

The one combination that is refused up front is `outputType: "preview"` together with `mailing`: a preview render deletes its own file immediately, so there would be nothing to attach.

---

<h3 id="excel" style="color: #203671; margin-top: 2.2em;">The Excel attachment</h3>

`includeExcel` turns the data of the render into a real xlsx, not a CSV with a different name. The rows come from what was actually rendered, which matters for reports fed by an SQL adapter: there the rows only exist in the render result, never in your request.

The sheet has a header row in the report's accent colour, frozen below the header, with an auto filter across the table. Columns are typed from the report's field definitions, so an integer is a number, a date is a real Excel date and a decimal keeps its format. For a dynamic array without field definitions the type is inferred from the values. Rows with differing keys are merged into one header, in first-seen order, and missing values stay empty.

With `includeParameters` the parameters are printed above the table, so the recipient can see what the report was asked for without opening the mail again.

If the render produced a history record, the xlsx is stored on it and stays downloadable from the history record page. It is cleaned up by the same retention as the PDF.

---

<h3 id="placeholders" style="color: #203671; margin-top: 2.2em;">Placeholders in recipients and attachment names</h3>

Subject and body are placeholder-driven, and so are the **recipients**, the **contact name** and the **attachment names**. They all accept the same catalog, which is what makes mailing useful for data that only exists after the render.

Every route that takes recipients accepts them: the `mailing` segment of a render, `POST /report-history-record/{id}/mail`, `POST /report-mail-task` and the repeat endpoint. The failure recipients of a schedule are the exception, they are plain addresses, because a run that failed has no render to resolve against.

```
"to": ["[data.first.customerEmail]"],
"contactName": "[data.first.customerName]",
"pdfFileName": "Lieferschein_[parameters.P_ORDER_NO]_[now.date]"
```

For a report with an SQL adapter, `[data.first.<column>]` addresses the rows the query fetched. The recipient list is therefore a result of the render, not an input to it.

A few rules are worth knowing:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Situation</th>
      <th style="text-align: left; padding: 6px 10px;">Behaviour</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">A placeholder holds several addresses</td>
      <td style="padding: 6px 10px;">Comma or semicolon separated values are split, trimmed and de-duplicated. One column can address a whole distribution list.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">A <code>cc</code> or <code>bcc</code> placeholder resolves to nothing</td>
      <td style="padding: 6px 10px;">The address is left out and the mail goes anyway. A render reports it in <code>meta.mailing.unresolvedPlaceholders</code>, <code>POST /report-mail-task</code> as an <em>Unresolved placeholder</em> note in <code>meta.messages</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">No valid <code>to</code> address is left, or one resolves to something that is not an address</td>
      <td style="padding: 6px 10px;">In a render no mail is sent, the reason is in <code>meta.mailing.errors</code> and the render itself is unaffected. On <code>POST /report-mail-task</code> the call answers 422 and no task is created.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">What the mail task stores</td>
      <td style="padding: 6px 10px;">The resolved address list, not the placeholder. <code>[recipients.toCount]</code> and the other counters in subject and body therefore count real recipients.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Column names</td>
      <td style="padding: 6px 10px;">Matched exactly as the adapter returns them. <code>[data.first.EMAIL]</code> is not <code>[data.first.email]</code>.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;">Attachment names</td>
      <td style="padding: 6px 10px;">Resolved, then made file-system safe with case and underscores preserved. Umlauts are transliterated, invalid characters collapse to a single hyphen, the extension is forced.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Placeholder values are inserted as text.</strong> A resolved value that happens to contain markup, for example a customer name with a <code>&lt;b&gt;</code> in it, appears literally in the mail instead of changing its formatting. The markup you set in the editor is unaffected.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>The name is a label, not a file.</strong> <code>pdfFileName</code> and <code>excelFileName</code> only change what the attachment is called in the mail. On disk the PDF and the xlsx are always stored under the trace id, they hang on the history record, stay downloadable from it and are deleted with it. Two mails may carry the same attachment name without anything being overwritten.
</div>

Without a requested name the attachment is called like the stored file, which is the trace id plus its extension.

---

<h3 id="sync" style="color: #203671; margin-top: 2.2em;">Immediately or through the queue</h3>

By default the mail is sent while the request is still open. The response then already tells you whether it went out. That is the right mode for a single document and a fast relay.

`sendAsync: true` creates the mail task, hands it to the queue and answers immediately. The mail is sent by a Horizon worker, the task moves from *Pending* to *Sent*, and a failure is retried with backoff before it ends as *Error*. Use it for bulk runs and for slow or unreliable SMTP servers, where waiting for the transport would block the render.

<div style="border-left: 4px solid #F9C846; background: #fffdf3; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>Asynchronous sending needs a running queue.</strong> Without <code>php artisan horizon</code> the mail task stays <em>Pending</em> forever. It is visible in the Mail Queue, so nothing is lost, but nothing is sent either.
</div>

There is a third case you do not ask for: if the mailer's rate limit is exhausted, a synchronous send is converted into a delayed queued send instead of failing. The mail is not lost, the response says so, and the task shows when it will go out. See the Mailers page for the windows.

---

<h3 id="frontend" style="color: #203671; margin-top: 2.2em;">Mailing from the frontend</h3>

The same settings are available in four places in the UI, built from one shared block of fields, so they look and behave identically everywhere.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Place</th>
      <th style="text-align: left; padding: 6px 10px;">How it works</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Generate PDF</td>
      <td style="padding: 6px 10px;">A <strong>Send results via Mail</strong> toggle in the Output Settings. Configure it once, and every render from that page mails its result.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Scan2Print</td>
      <td style="padding: 6px 10px;">The same toggle in the station configuration. Every scan then prints and mails.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Report History Record</td>
      <td style="padding: 6px 10px;">A <strong>Send Mail</strong> button that mails a document which was rendered earlier, with a freely chosen mailer, template and recipients.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Job Scheduler</td>
      <td style="padding: 6px 10px;">The same toggle on a render job. Every run of the schedule mails its result.</td>
    </tr>
  </tbody>
</table>

In the configuration modals the mailing settings live in the session alongside the rest of the output settings, and the values are kept even while the toggle is off, so switching mailing off for a moment loses nothing. The file name fields appear only while their attachment switch is on.

After a render the notification says what happened to the mail. A mail that did not go out as configured turns the success notification into a warning instead of hiding the problem.

The block is only rendered at all when the current user may create mail tasks and at least one active mailer and one active template exist. Nobody is offered a button that cannot work.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mailing.generate-pdf-settings.png</code><br>Output Settings of the Generate PDF page with the mailing block expanded.
</div>

---

<h3 id="on-a-schedule" style="color: #203671; margin-top: 2.2em;">On a schedule</h3>

A mail does not need a caller. A render job in the **Job Scheduler** carries the same mailing settings as a render request, so a schedule puts the shift report in a mailbox at six every morning without anybody asking for it.

Two things work differently from an interactive send. The schedule's owner and API token decide what may go out: the token needs `report-mail-task:create`, and a revoked token or a deactivated owner parks the schedule rather than mailing. And placeholders are resolved at the moment the run fires, so `[now.date]` in a subject line or an attachment name carries the date of that run, not the date the schedule was saved.

Every run is logged with its trace id. A mail task that ends in *Error*, or anything reported in `meta.mailing`, marks the run as a warning, so a schedule whose mail is not arriving is visible in the run log without opening the Mail Queue.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Two different mails.</strong> The mail configured here is the document itself. A schedule that could not be executed at all sends the Job Scheduler's own failure mail to its own recipients. See <em>Schedule your jobs</em>.
</div>

---

<h3 id="queue" style="color: #203671; margin-top: 2.2em;">The Mail Queue</h3>

Every send attempt, synchronous or queued, is recorded as a **Report Mail Task**. The overview lives in the main menu under **Mail Queue** and is a regular VeloxFactory lookup with the usual filters and the usual session-persisted state.

A mail task keeps what was actually sent, not just what was requested: the resolved recipients, the rendered subject, the rendered body and the names of the attachments. The detail page shows the mail exactly as the recipient received it, rendered in an iframe from the stored HTML, whitelabeling included.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Status</th>
      <th style="text-align: left; padding: 6px 10px;">Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Pending</strong></td>
      <td style="padding: 6px 10px;">Queued, or waiting for a free rate limit slot. The task shows when it is due.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Sent</strong></td>
      <td style="padding: 6px 10px;">The transport accepted the message, with the timestamp. Delivery itself is not tracked.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Error</strong></td>
      <td style="padding: 6px 10px;">The send failed after its retries. The transport's own message is on the task.</td>
    </tr>
  </tbody>
</table>

**Repeat** resends a task. It opens a dialog in which mailer and recipients can be changed before sending, so a mail that went to the wrong address is corrected rather than cloned. Subject and body are re-rendered from the current template when the history record still exists, otherwise the stored snapshot is reused. The repeated task gets its own trace id, derived from the original.

The same table also appears on the Report History Record page, so the mails belonging to one document are visible where the document is.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mailing.mail-queue.png</code><br>Mail Queue overview with status badges, recipients and the repeat action.
</div>

---

<h3 id="retention" style="color: #203671; margin-top: 2.2em;">Retention</h3>

Mail tasks are cleaned up by the **Purge Job**, the consolidated cleanup schedule of the Job Scheduler. It removes print tasks and mail tasks before the history records they reference, so nothing is deleted out from under a reference. How long mail tasks are kept is a retention on that schedule, editable in the frontend or through the API like everything else; an empty value switches the step off entirely.

The same work is available as a standalone command:

```bash
php artisan report-mail-tasks:purge --days=30
```

Attachments are not owned by the mail task. The PDF and the xlsx belong to the history record and are removed by its retention step, or, if no history record was created, by the orphaned-file step.

---

<h3 id="api" style="color: #203671; margin-top: 2.2em;">The API</h3>

Besides the `mailing` segment on `POST /report-config/{id}/render`, mailing has endpoints of its own.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Endpoint</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /report-history-record/{id}/mail</code></td>
      <td style="padding: 6px 10px;">Mail a document that was rendered earlier. Takes the same fields as the mailing segment, including placeholders in recipients and attachment names. Builds the xlsx on the fly if the record does not have one yet.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /report-mail-task</code></td>
      <td style="padding: 6px 10px;">List mail tasks, filterable by status.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /report-mail-task/{id}</code></td>
      <td style="padding: 6px 10px;">A single mail task, with its relations and its audit segment on request.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /report-mail-task</code></td>
      <td style="padding: 6px 10px;">Create and dispatch a mail task directly. <code>to</code>, <code>cc</code> and <code>bcc</code> take literal addresses or placeholders, resolved against the render of the referenced history record.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /report-mail-task/{id}/repeat</code></td>
      <td style="padding: 6px 10px;">Resend a task. Optional <code>mailer</code>, <code>to</code>, <code>cc</code> and <code>bcc</code> override the original values, anything left out is reused. The overriding recipients accept placeholders too, resolved against the history record behind the task; if the task has none, only the generic placeholders such as <code>[user.email]</code> resolve.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>DELETE /report-mail-task/{id}</code></td>
      <td style="padding: 6px 10px;">Delete a mail task. The attachments belong to the history record and are not touched.</td>
    </tr>
  </tbody>
</table>

A render that mailed something returns the mail task in its response, and anything noteworthy about the mailing in `meta.mailing`:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Key</th>
      <th style="text-align: left; padding: 6px 10px;">Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>errors</code></td>
      <td style="padding: 6px 10px;">The mail was not sent, with the reason.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>skipped</code></td>
      <td style="padding: 6px 10px;">The render failed, so there was nothing to send.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>excelError</code></td>
      <td style="padding: 6px 10px;">The xlsx could not be built. The mail goes out with the PDF only.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>unresolvedPlaceholders</code></td>
      <td style="padding: 6px 10px;">Placeholders in recipients, contact name or attachment names that resolved to nothing.</td>
    </tr>
  </tbody>
</table>

---

<h3 id="mcp" style="color: #203671; margin-top: 2.2em;">AI assistants</h3>

The MCP server exposes mailing the same way it exposes printing: the `mailing` segment is part of the render tool, and mail tasks, mailers and templates have their own tools. Because mailer and template are resolved by name, an assistant can pass what a person said. *Render the delivery note for order 4711 and mail it to dispatch* is one call.

---

<h3 id="permissions" style="color: #203671; margin-top: 2.2em;">Permissions</h3>

Mailing has three permission scopes, each with the usual `read`, `create`, `update`, `delete` and `full`.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Scope</th>
      <th style="text-align: left; padding: 6px 10px;">Grants</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mailer:*</code></td>
      <td style="padding: 6px 10px;">The Mailers master data.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mail-template:*</code></td>
      <td style="padding: 6px 10px;">The Mail Templates master data.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>report-mail-task:*</code></td>
      <td style="padding: 6px 10px;">Seeing the Mail Queue, and <code>create</code> for actually sending mail, from the API as well as from the frontend.</td>
    </tr>
  </tbody>
</table>

`report-mail-task:create` is the one that matters for sending. It is checked at the start of a render, before anything is rendered, so a user without it gets a clear refusal instead of a wasted render.

Mail graphics are Common Report Resources, so maintaining them uses `common-report-resource:*`. That permission also covers the logos printed on labels, which is the price for uploading a logo once rather than twice.

</div>

# Mailers: master data for your SMTP accounts

<div style="text-align: justify;">

A mail needs an account to go out through. **Mailers** are that account as master data: one record per SMTP mailbox or relay, with its host, its credentials, the address it sends from, and the limits your provider actually enforces. Every mail VeloxFactory sends goes through one of these records.

The `MAIL_*` keys in the environment file are Laravel's own fallback mailer and are deliberately **not** used by report mailing. Mail configuration belongs in the application, where it can be changed, tested and audited without a deployment.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mailer.index.png</code><br>Mailers overview under Configuration with transport badges, the approval status and the Active column.
</div>

---

<h3 id="where" style="color: #203671; margin-top: 2.2em;">Where to find it</h3>

Mailers live in the main menu under **Configuration → Mailers**. The overview is a regular VeloxFactory lookup: search over name, host, sender address and description, filters for transport, active state, tested state, creator, updater and creation date, sortable columns, and the filter state kept in your session.

**New** opens the create form, the pencil on a row opens an existing record. Deleting is done from the edit page.

---

<h3 id="fields" style="color: #203671; margin-top: 2.2em;">The fields of a mailer</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Field</th>
      <th style="text-align: left; padding: 6px 10px;">Required</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Name</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">Unique, three to 55 characters, for example <code>Office SMTP</code>. This is what a render request may use instead of the ID.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Transport</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;"><code>smtp</code> for a real server, <code>sendmail</code> for the local binary, <code>log</code> to write the mail into the log instead of sending it. <code>log</code> is what makes a demo or a staging system harmless.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Host, Port</td>
      <td style="padding: 6px 10px;">for smtp</td>
      <td style="padding: 6px 10px;">The server and its port, for example <code>smtp.office365.com</code> and <code>587</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Scheme</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;"><code>smtp</code> for STARTTLS on port 587, <code>smtps</code> for implicit TLS on port 465. Left empty the transport decides by port, which is right for most servers.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Username, Password</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Stored encrypted. The password is never returned by the API, and the edit form shows it empty: leave it empty to keep the stored one.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">From Address</td>
      <td style="padding: 6px 10px;">yes</td>
      <td style="padding: 6px 10px;">The sender of every mail sent through this mailer. Many providers require it to match the authenticated mailbox.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">From Name</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">The display name next to the address, for example <code>Shipping Department</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Reply To</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Where replies should go, when that is not the sender. Useful for a no-reply sender with a real support mailbox behind it.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Timeout</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Seconds before the transport gives up. Relevant for synchronous sending, where the render request waits.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Local Domain</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">The EHLO name. Left empty the host part of the application URL is used. Some strict relays insist on a specific value.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Limit per minute / hour / day</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">The dispatch rate limits, see below. The hourly limit defaults to 500.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Description</td>
      <td style="padding: 6px 10px;">no</td>
      <td style="padding: 6px 10px;">Free note, for example which department the mailbox belongs to.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Active</td>
      <td style="padding: 6px 10px;">-</td>
      <td style="padding: 6px 10px;">On by default. An inactive mailer stays in the master data and keeps its history, but it can no longer be selected and no longer resolves in a render request.</td>
    </tr>
  </tbody>
</table>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mailer.edit.png</code><br>Mailer edit form with connection fields, the three rate limit fields and the remaining-slots badges.
</div>

---

<h3 id="test" style="color: #203671; margin-top: 2.2em;">Testing a mailer</h3>

A new or changed mailer starts as **Unapproved**. The edit page has a **Send Test Mail** button that sends a short message through exactly the configuration you just saved, to your own address by default. On success the mailer flips to **Approved**, on failure the transport's own error message is shown, which is usually enough to see whether it was the port, the credentials or the TLS mode.

This mirrors how report connections are verified, and it works the same way: the status is information, not a gate. A mailer that has never been tested can still be used, it is simply not marked as proven.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Changing a connection field resets the approval.</strong> Transport, host, port, scheme, credentials and sender address all invalidate the tested state, so a green badge always refers to the configuration that is actually stored.
</div>

A test mail consumes a rate limit slot like any other mail, so the counter stays honest. If the limit is exhausted, the test is refused with the waiting time instead of being sent.

---

<h3 id="ratelimit" style="color: #203671; margin-top: 2.2em;">Rate limits</h3>

Providers cap outbound mail, and they all cap differently. Exceeding the cap does not produce a polite error, it produces a blocked account, so the limits belong on the mailer, next to the credentials they protect.

Each mailer has three independent windows, each of them optional:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Window</th>
      <th style="text-align: left; padding: 6px 10px;">Typical for</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">per minute</td>
      <td style="padding: 6px 10px;">Relays that cap bursts, for example Office 365 at roughly 30 messages per minute.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">per hour</td>
      <td style="padding: 6px 10px;">Shared hosting mailboxes, typically 200 to 500 per hour. This is the one that defaults to 500.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">per day</td>
      <td style="padding: 6px 10px;">Mailbox providers, for example Gmail at 500 per day.</td>
    </tr>
  </tbody>
</table>

An empty field means the window is not enforced. All three empty means the mailer sends unthrottled. Check your provider's documentation and configure slightly below its cap, because the windows here are fixed rather than sliding.

A slot is spent per **message**, not per recipient: a mail to five people costs one slot, which is how providers count as well.

When a slot is not available, VeloxFactory does not drop the mail:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Path</th>
      <th style="text-align: left; padding: 6px 10px;">What happens</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Synchronous</td>
      <td style="padding: 6px 10px;">The mail is converted into a delayed queued send. The response says so, the task stays <em>Pending</em> and shows when it is due.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Asynchronous</td>
      <td style="padding: 6px 10px;">The queued job releases itself until the window opens. Waiting does not count against the retry budget, so throttling never turns into an error.</td>
    </tr>
  </tbody>
</table>

The edit page shows the remaining slots per configured window, so a run that is about to hit a cap is visible before it does.

<div style="border-left: 4px solid #F9C846; background: #fffdf3; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
⚠️ <strong>The queue fallback needs a running queue.</strong> If a synchronous send is pushed into the queue because of the rate limit and no Horizon worker is running, the mail waits as <em>Pending</em> in the Mail Queue. Nothing is lost, but nothing is sent either.
</div>

Counters are per mailer and per installation. Two VeloxFactory instances sharing one SMTP account do not share a counter.

---

<h3 id="resolution" style="color: #203671; margin-top: 2.2em;">Name resolution and deletion</h3>

A render request may address a mailer by its numeric ID or by its unique name, and only **active** records resolve. An unknown or inactive name is refused with a clear message before anything is rendered, rather than silently falling back to some default mailer.

A mailer cannot be deleted while a mail task still references it, so the history of what was sent stays intact. To retire a mailbox, switch the record to inactive: it disappears from every picker and stops resolving, while everything that was sent through it keeps its record.

---

<h3 id="api" style="color: #203671; margin-top: 2.2em;">The API</h3>

Mailers are a regular API resource under `/api/v1/mailer`, with the same envelope, the same audit segment and the same error format as every other resource.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Endpoint</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /mailer</code></td>
      <td style="padding: 6px 10px;">List mailers, optionally narrowed with <code>limit</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /mailer/{id}</code></td>
      <td style="padding: 6px 10px;">A single mailer, with <code>withAudit=true</code> including its audit segment.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /mailer</code></td>
      <td style="padding: 6px 10px;">Create a mailer. <code>name</code>, <code>transport</code> and <code>fromAddress</code> are required, <code>host</code> and <code>port</code> additionally for SMTP.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /mailer/{id}/test</code></td>
      <td style="padding: 6px 10px;">Send a test mail, optionally to a given <code>to</code> address, and update the approval status.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>PATCH /mailer/{id}</code></td>
      <td style="padding: 6px 10px;">Partial update. An omitted or empty password keeps the stored one.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>DELETE /mailer/{id}</code></td>
      <td style="padding: 6px 10px;">Delete a mailer, refused while mail tasks still reference it.</td>
    </tr>
  </tbody>
</table>

A mailer resource is returned as:

```json
{
    "model": "Mailer",
    "id": 1,
    "name": "Office SMTP",
    "transport": "smtp",
    "host": "smtp.office365.com",
    "port": 587,
    "scheme": "smtp",
    "username": "reports@example.com",
    "fromAddress": "reports@example.com",
    "fromName": "Shipping Department",
    "replyTo": "support@example.com",
    "timeout": 30,
    "localDomain": null,
    "rateLimitPerMinute": 30,
    "rateLimitPerHour": 500,
    "rateLimitPerDay": null,
    "rateLimitRemaining": { "minute": 28, "hour": 471 },
    "isActive": true,
    "mailerTested": true,
    "status": "Approved",
    "description": "Shipping mailbox"
}
```

The password is never part of the response.

---

<h3 id="mcp" style="color: #203671; margin-top: 2.2em;">AI assistants</h3>

The MCP server exposes the same operations as tools, including the test. An assistant can create a mailer from a provider's documented settings and verify it in the same conversation.

---

<h3 id="permissions" style="color: #203671; margin-top: 2.2em;">Permissions</h3>

Mailers use the standard permission scheme and have their own **Mailers** section in the user editor.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Permission</th>
      <th style="text-align: left; padding: 6px 10px;">Grants</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mailer:read</code></td>
      <td style="padding: 6px 10px;">See the menu entry, the overview and a mailer's detail page. Also required to pick a mailer when sending.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mailer:create</code></td>
      <td style="padding: 6px 10px;">Create mailers.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mailer:update</code></td>
      <td style="padding: 6px 10px;">Change mailers, including the active state, and send test mails.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mailer:delete</code></td>
      <td style="padding: 6px 10px;">Delete mailers.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mailer:full</code></td>
      <td style="padding: 6px 10px;">All of the above, like <code>global:admin</code>.</td>
    </tr>
  </tbody>
</table>

Credentials are readable to anyone with `mailer:read` in the sense that the username is shown; the password is not, on any path. Treat `mailer:update` as an administrative permission.

</div>

# Mail Templates: subject, body and whitelabeling

<div style="text-align: justify;">

A mail needs something to say. **Mail Templates** are the subject and the body, written once and reused by every render that mails a document. They are edited in a WYSIWYG editor, so nobody has to write HTML, and everything variable is a placeholder that VeloxFactory fills in at send time: the trace id, a parameter of the request, the name of the report, a column of the data that was rendered.

A template decides the content. The surrounding mail, a plain card in your application's colours, is built by VeloxFactory, and a template can override parts of it when a customer needs their own look.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mail-template.edit.png</code><br>Mail Template edit page with the rich text editor, the toolbar and the placeholder and preview buttons.
</div>

---

<h3 id="where" style="color: #203671; margin-top: 2.2em;">Where to find it</h3>

Mail Templates live in the main menu under **Configuration → Mail Templates**. The overview is a regular VeloxFactory lookup with search over name, subject and description, a filter for the active state and the usual audit filters.

A template has a unique name, which is what a render request may use instead of the ID, a subject, a body and an active flag. An inactive template stays in the master data but is no longer offered and no longer resolves.

---

<h3 id="editor" style="color: #203671; margin-top: 2.2em;">The editor</h3>

The body is written in a rich text editor built into VeloxFactory. No add-on, no external service, and nothing to learn: the toolbar does what a toolbar does.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Group</th>
      <th style="text-align: left; padding: 6px 10px;">Tools</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Text</td>
      <td style="padding: 6px 10px;">Bold, italic, underline, headings, paragraph, bullet and numbered lists, text colour and remove colour, clear formatting.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Links</td>
      <td style="padding: 6px 10px;">Insert and remove links. Only <code>http</code>, <code>https</code> and <code>mailto</code> are accepted, and every link is marked so it opens safely.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Tables</td>
      <td style="padding: 6px 10px;">Insert a table with a chosen number of rows and columns and an optional header row, then insert or delete rows and columns from wherever the caret sits.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Images</td>
      <td style="padding: 6px 10px;">Insert a graphic from the Common Report Resources, with an optional width.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Placeholders</td>
      <td style="padding: 6px 10px;">Insert a placeholder at the caret, chosen from the catalog.</td>
    </tr>
  </tbody>
</table>

Pasting is deliberately plain text. Formatted content dragged in from Word or a browser is the single most common reason a mail looks broken in one client and fine in another, so the editor strips it and keeps the words.

Whatever the editor produces is cleaned on the server against an allowlist before it is stored. That is not a formality: it is what guarantees that a template cannot carry a script, a tracking pixel or an arbitrary external image, no matter what was pasted into it.

---

<h3 id="placeholders" style="color: #203671; margin-top: 2.2em;">Placeholders</h3>

Anything in square brackets is replaced when the mail is built. Both the subject and the body support them.

> Hello [contactName], attached is your report [report.name] with the trace ID [traceId], rendered on [now.dateLocale].

The **Placeholders** button opens a dialog listing every token that is available, grouped, with a description and an example, and each one copyable with a click. Opened from a template that is tied to a report, the parameter and field tokens are expanded to the real names of that report.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Group</th>
      <th style="text-align: left; padding: 6px 10px;">Examples</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">General</td>
      <td style="padding: 6px 10px;"><code>[traceId]</code>, <code>[reportFileName]</code> (the attachment name), <code>[reportUrl]</code>, <code>[outputType]</code>, <code>[appName]</code>, <code>[contactName]</code>, <code>[broadcastId]</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Generic</td>
      <td style="padding: 6px 10px;"><code>[now.date]</code>, <code>[now.dateLocale]</code>, <code>[now.dateTime]</code>, <code>[now.weekday]</code>, <code>[now.monthName]</code>, <code>[now.weekNumber]</code>, <code>[now.quarter]</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Report</td>
      <td style="padding: 6px 10px;"><code>[report.name]</code>, <code>[report.description]</code>, <code>[report.context]</code>, <code>[report.connection]</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">History Record</td>
      <td style="padding: 6px 10px;"><code>[historyRecord.id]</code>, <code>[historyRecord.status]</code>, <code>[historyRecord.url]</code> as a direct link into VeloxFactory</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">User, API Token</td>
      <td style="padding: 6px 10px;"><code>[user.name]</code>, <code>[user.email]</code>, <code>[token.name]</code> - who or what triggered the render</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Printer</td>
      <td style="padding: 6px 10px;"><code>[printer.name]</code>, <code>[printer.copies]</code> when a print task was created alongside</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Parameters, Data</td>
      <td style="padding: 6px 10px;"><code>[parameters.P_ORDER_NO]</code>, <code>[data.count]</code>, <code>[data.first.CUSTOMER]</code>, <code>[data.last.ARTICLE]</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Recipients</td>
      <td style="padding: 6px 10px;"><code>[recipients.toCount]</code>, <code>[recipients.ccCount]</code>, <code>[recipients.bccCount]</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Environment</td>
      <td style="padding: 6px 10px;"><code>[env.appEnv]</code>, <code>[env.hostname]</code>, <code>[env.timezone]</code> - to mark a mail from a test system as such</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Images</td>
      <td style="padding: 6px 10px;"><code>[image.logo_dark]</code> - one entry per usable Common Report Resource</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>An unknown placeholder never breaks a mail.</strong> It resolves to an empty string and is reported back to the caller, so a typo shows up as a gap and a note, not as a failed send.
</div>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Placeholder values are inserted as text.</strong> A resolved value that happens to contain markup, for example a customer name with a <code>&lt;b&gt;</code> in it, appears literally in the mail instead of changing its formatting. The markup you set in the editor is unaffected.
</div>

The same catalog is available beyond subject and body: recipients, contact name and the names the attachments carry in the mail accept placeholders too. See the Report Mailing page for how that behaves.

---

<h3 id="images" style="color: #203671; margin-top: 2.2em;">Graphics</h3>

Mail graphics are the **Common Report Resources** you already maintain, the same PNG and JPEG files the reports use. A logo is uploaded once and serves both a printed label and a mail.

Insert one with the image button, or type its placeholder, for example `[image.logo_dark]`. Both produce the same thing, and the editor shows the graphic while you write.

At send time the graphic is embedded **into** the mail as an inline attachment, not linked from a server. That matters for two reasons: the mail is self-contained, so it also works for a recipient outside your network, and no client shows a *load external images* bar or treats it as a tracking pixel.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Only graphics from the master data.</strong> An image pasted in from a website is removed when the template is saved. If a graphic should be in a mail, upload it under Configuration → Common Report Resources and reference it by name.
</div>

A Common Report Resource that a template embeds cannot be deleted or renamed while the template uses it. The refusal names the template, so it is clear what would break.

---

<h3 id="whitelabel" style="color: #203671; margin-top: 2.2em;">Whitelabeling</h3>

By default every mail looks the same: a card in the colours of your VeloxFactory installation, read straight from the application's stylesheet, with a coloured header bar and a footer. Change a colour in the frontend and the mails follow, without touching a template.

When one customer or one process needs something else, a template can override parts of that frame. Each override has its own switch, and switching it off returns that part to the global default:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Override</th>
      <th style="text-align: left; padding: 6px 10px;">Effect</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Header colour</td>
      <td style="padding: 6px 10px;">The colour of the bar at the top of the card, instead of the theme colour.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Body colour</td>
      <td style="padding: 6px 10px;">The background of the content area, instead of white.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Logo</td>
      <td style="padding: 6px 10px;">A different Common Report Resource in the header, instead of the configured logo.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Hide logo</td>
      <td style="padding: 6px 10px;">No logo at all, for a plain header.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Footer text</td>
      <td style="padding: 6px 10px;">A different footer line. Supports the full placeholder catalog, so it can carry a trace id, a contact or a legal note.</td>
    </tr>
  </tbody>
</table>

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>mail-template.whitelabel.png</code><br>The whitelabeling block with the per-field switches for header colour, body colour, logo and footer text.
</div>

---

<h3 id="preview" style="color: #203671; margin-top: 2.2em;">Preview</h3>

The **Preview** button renders the template inside the real mail frame, with example values or against an existing history record, and shows it in a dialog. It is the same rendering path the actual mail uses and the same one the Mail Queue uses to show a sent mail, so the three cannot drift apart: what the preview shows is what is sent and what is archived.

Use it after every change. It is the fastest way to catch a placeholder that resolves to nothing, a table that lost its borders or a whitelabel colour that makes the text unreadable.

---

<h3 id="api" style="color: #203671; margin-top: 2.2em;">The API</h3>

Mail Templates are a regular API resource under `/api/v1/mail-template`.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Endpoint</th>
      <th style="text-align: left; padding: 6px 10px;">Description</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /mail-template/placeholders</code></td>
      <td style="padding: 6px 10px;">The placeholder catalog, optionally expanded for a given <code>reportConfig</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /mail-template</code></td>
      <td style="padding: 6px 10px;">List templates.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /mail-template/{id}</code></td>
      <td style="padding: 6px 10px;">A single template, with its audit segment on request.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /mail-template</code></td>
      <td style="padding: 6px 10px;">Create a template. The body is sent Base64-encoded and is cleaned against the allowlist before it is stored.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /mail-template/{id}/preview</code></td>
      <td style="padding: 6px 10px;">Render subject and body, with example values or against a history record, and report which placeholders stayed unresolved.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>PATCH /mail-template/{id}</code></td>
      <td style="padding: 6px 10px;">Partial update.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>DELETE /mail-template/{id}</code></td>
      <td style="padding: 6px 10px;">Delete a template, refused while mail tasks still reference it.</td>
    </tr>
  </tbody>
</table>

Unknown placeholders in a stored template are not an error. They are returned as messages, so an integration can warn without being blocked.

---

<h3 id="mcp" style="color: #203671; margin-top: 2.2em;">AI assistants</h3>

The MCP server exposes the same operations, including the placeholder catalog. An assistant can therefore write a template that uses the real parameter names of a specific report, and preview it before anyone sends it.

---

<h3 id="permissions" style="color: #203671; margin-top: 2.2em;">Permissions</h3>

Mail Templates use the standard permission scheme and have their own **Mail Templates** section in the user editor.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Permission</th>
      <th style="text-align: left; padding: 6px 10px;">Grants</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mail-template:read</code></td>
      <td style="padding: 6px 10px;">See the menu entry, the overview and a template. Also required to pick a template when sending.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mail-template:create</code></td>
      <td style="padding: 6px 10px;">Create templates.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mail-template:update</code></td>
      <td style="padding: 6px 10px;">Change templates, including the whitelabeling and the active state.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mail-template:delete</code></td>
      <td style="padding: 6px 10px;">Delete templates.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>mail-template:full</code></td>
      <td style="padding: 6px 10px;">All of the above, like <code>global:admin</code>.</td>
    </tr>
  </tbody>
</table>

Uploading the graphics a template uses is a separate permission, `common-report-resource:create` and `:update`, because those files are shared with the reports.

</div>

# Schedule your jobs

The Job Scheduler: renders and the consolidated cleanup, on a crontab expression or at a single date and time, operable through the API.

# The Job Scheduler: work that happens without you

<div style="text-align: justify;">

Most of what VeloxFactory does happens because something asked for it: a request comes in, a report is rendered, a label goes to a printer. Some work has no caller, though. A shift report that should be in the supervisor's mailbox at six every morning. The cleanup that keeps the history from growing forever. Work like that runs on a clock, not on a request.

The **Job Scheduler** is where that clock lives. Every job is a record with a timezone and an active flag, and it says when it runs either as a crontab expression or as a single date and time. Schedules are maintained in the frontend or through the API like any other master data. VeloxFactory itself runs one fixed entry: every minute it looks for schedules that are due and hands them to the queue.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scheduler.overview.png</code><br>The Job Scheduler overview with type, cron expression, next run, last run and status per schedule.
</div>

---

<h3 id="types" style="color: #203671; margin-top: 2.2em;">Two kinds of schedule</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Type</th>
      <th style="text-align: left; padding: 6px 10px;">What it does</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Render Job</strong></td>
      <td style="padding: 6px 10px;">Fires a complete render request on a schedule: parameters, data rows, printing, history record and the full mailing segment. Everything a render can do from the API, a schedule can do at four in the morning.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Purge Job</strong></td>
      <td style="padding: 6px 10px;">The housekeeping: print tasks, mail tasks, history records, orphaned files and the scheduler's own run log, each with its own retention in days. One schedule, one run, a fixed order.</td>
    </tr>
  </tbody>
</table>

One Purge Job covers all five kinds of cleanup, so there is a single place to say when housekeeping happens and how long each entity is kept. A fresh instance already has the schedule, deactivated and without a cron expression, so nothing runs until you decide when it should.

---

<h3 id="cron" style="color: #203671; margin-top: 2.2em;">Saying when</h3>

A schedule accepts full five field crontab syntax, the same thing every operations person already knows. You do not have to write it by hand: the editor has a guided mode with frequency, time and weekday pickers, and an expression mode for everything the guided mode cannot express. Both write into the same field, and whichever you use, the next run times are shown underneath while you type.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Expression</th>
      <th style="text-align: left; padding: 6px 10px;">Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>0 6 * * 1-5</code></td>
      <td style="padding: 6px 10px;">Weekdays at 06:00, the shift report.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>*/15 * * * *</code></td>
      <td style="padding: 6px 10px;">Every quarter of an hour.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>0 3 1 * *</code></td>
      <td style="padding: 6px 10px;">The first of every month at 03:00, the monthly statement.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">empty</td>
      <td style="padding: 6px 10px;">Never fires on its own. The schedule exists and can be started by hand, which is how you test one before switching it on.</td>
    </tr>
  </tbody>
</table>

Every schedule carries its own timezone, defaulting to the timezone of the instance. That matters for a company that renders for a plant in another country: the expression says 06:00 and the timezone says which 06:00.

<div style="border-left: 4px solid #F9C846; background: #fffbf0; padding: 12px 16px; margin: 16px 0; color: #525E5A;">
<strong>Daylight saving time:</strong> an expression that points into the hour the clock skips or repeats runs twice in October and not at all in March. For a job that has to happen exactly once a day, pick a time outside 02:00 to 03:00.
</div>

---

<h3 id="once" style="color: #203671; margin-top: 2.2em;">Running once, at a given time</h3>

Not every job repeats. A price list that has to go out on the day the new catalogue takes effect, a cleanup that belongs to one migration: those are a point in time, not a pattern. A schedule therefore says when it runs either with a cron expression or with a single date and time, never with both.

```json
{
    "runAt": "2026-10-25 17:35",
    "deactivateAfterRun": true
}
```

In the form the field is **Run once at**, with a date and a time picker, next to the switch **Deactivate after the run**. A one-time schedule shows *Once* in the overview instead of an expression, and its date on the detail page and in the next run preview.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Question</th>
      <th style="text-align: left; padding: 6px 10px;">Answer</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Which clock does the time follow?</td>
      <td style="padding: 6px 10px;">The timezone of the schedule, or the timezone of the instance when the schedule has none. A value that carries its own offset, such as <code>2026-10-25T17:35:00+02:00</code>, wins over both. The API answers with the time in the schedule's timezone.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Cron and date together?</td>
      <td style="padding: 6px 10px;">Refused with 422. To turn a one-time schedule into a recurring one, send <code>runAt: null</code> together with the cron expression.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">A date in the past?</td>
      <td style="padding: 6px 10px;">A date that is being set or changed has to lie in the future. A date that already passed does not block edits to the other fields of the schedule.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">What happens after the run?</td>
      <td style="padding: 6px 10px;">The date is used up: there is no next run and the schedule does not fire again. That also holds when the slot was discarded after a long outage of the scheduler, or when the worker died in the middle of the run.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">And the schedule itself?</td>
      <td style="padding: 6px 10px;">With <strong>Deactivate after the run</strong> it sets itself inactive. Without it the schedule stays active but has nothing pending; give it a new date to arm it again.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;">Does <strong>Run now</strong> use the date up?</td>
      <td style="padding: 6px 10px;">No. A manual run is a test, the pending date stays where it is.</td>
    </tr>
  </tbody>
</table>

A one-time Purge Job counts as an active cleanup for as long as its date is still ahead, so the overview does not warn about missing housekeeping while one is armed.

---

<h3 id="firing" style="color: #203671; margin-top: 2.2em;">What happens when a schedule is due</h3>

Every minute VeloxFactory collects the schedules whose next run has arrived, moves each one to its following slot and hands it to the queue. The order matters: the next slot is written first, so a schedule can never fire the same slot twice, whatever happens afterwards. A schedule that runs once has no following slot, so its slot is cleared instead of moved on, which is what makes it fire exactly once.

Three things are deliberately not done.

- **Missed slots are not made up.** If the scheduler was stopped for three hours, the slots in between are gone. One late run within the grace window is fired, anything older is recorded as skipped so you can see that it happened.
- **Runs of one schedule never overlap.** A render that takes longer than its own interval simply keeps running; the next run is recorded as skipped rather than queued behind it. Two different schedules do run side by side.
- **A run is not retried.** A second attempt would print a second label and send a second mail. A failed run ends as an error and says why.

---

<h3 id="identity" style="color: #203671; margin-top: 2.2em;">Who a render job runs as</h3>

A scheduled render is a real render request, so it needs somebody to make it. That somebody is the user who created the schedule, and the schedule carries one of that user's API tokens to run with. Nothing else changes: the same permissions apply, the same audit columns are filled, and the history record shows who and which token, exactly as if the request had come over HTTP.

Two consequences are worth knowing before you build a schedule around a colleague who is about to change roles.

- If the owner is set inactive, or the token is revoked or expires, the run stops before anything is rendered. It is recorded as skipped with the reason, and the failure mail goes out.
- Because a render job acts with its owner's permissions, only the owner and administrators may change its configuration or start it by hand. Everyone with the read permission can see it and read its log.

The token is referenced, never stored. VeloxFactory keeps only the hash of a token, so a schedule points at the token record you pick from a list, and revoking that token stops the schedule the same minute.

API tokens expire after the period configured in `SANCTUM_EXPIRATION`, one year by default. A schedule whose token has lapsed stops with the same error as a revoked one and sends the configured failure mail. Owners of long-running schedules should issue themselves a fresh token before the old one expires.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scheduler.render-form.png</code><br>The form of a render job: cron builder, report config, API token, output settings, parameters with the placeholder switch and the mailing block.
</div>

---

<h3 id="runlog" style="color: #203671; margin-top: 2.2em;">The run log</h3>

Every run writes a record, whether it worked or not. The log carries the trace id, so a scheduled render can be followed through the history record, the print task and the mail task it produced, and back again.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Status</th>
      <th style="text-align: left; padding: 6px 10px;">Means</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>Success</code></td>
      <td style="padding: 6px 10px;">Everything the schedule asked for happened.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>Warning</code></td>
      <td style="padding: 6px 10px;">The job ran, but something inside it did not: the render worked and the mail did not, a placeholder stayed empty, one purge step failed while the others finished.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>Error</code></td>
      <td style="padding: 6px 10px;">The job itself failed. The message from the API is on the record.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>Skipped</code></td>
      <td style="padding: 6px 10px;">The run never started: the previous one was still going, the owner or the token was not usable, or the slot was older than the grace window.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>Running</code></td>
      <td style="padding: 6px 10px;">In progress right now.</td>
    </tr>
  </tbody>
</table>

A run also records whether it was fired by the clock or started by hand, so a test run is never mistaken for a scheduled one.

---

<h3 id="failuremail" style="color: #203671; margin-top: 2.2em;">Being told when something breaks</h3>

A schedule that quietly stops working is worse than no schedule at all. Every schedule can name a mailer and a list of recipients for its failure mail, which goes out when a run ends as an error or is skipped. A warning does not mail: the render happened, and the detail is already on the history record or the mail task.

These recipients are plain addresses. Placeholders belong to the mail of a document, which is resolved from the render it belongs to, and a run that failed has no render to resolve anything against.

A schedule that fires every minute and fails every minute would otherwise fill a mailbox, so the failure mail is throttled to one per schedule per hour. The window is a setting of the instance, the runs in between count how many were suppressed, and the log still has every one of them.

---

<h3 id="mcp" style="color: #203671; margin-top: 2.2em;">AI assistants</h3>

The MCP server exposes the Job Scheduler the way it exposes the rest of VeloxFactory: schedules and their run log have their own tools, a crontab expression can be validated and its next run times read back before anything is saved, and a schedule can be started once by hand to see what it does. Because report configs, mailers and templates are resolved by name, a schedule is built from what a person said. *Send the shift report to the supervisor every weekday at six* is one request.

The retentions of the Purge Job have their own tool, because the API expects the complete retention map in every write and a single changed value would otherwise be refused.

---

<h3 id="permissions" style="color: #203671; margin-top: 2.2em;">Permissions</h3>

The Job Scheduler has one permission scope, `scheduled-job:*`, with the usual `read`, `create`, `update`, `delete` and `full`. The run log is covered by the same scope.

Two rules go beyond the usual pattern, both for the same reason: a schedule can act with somebody else's rights, or with nobody's.

- Creating a schedule implies being allowed to render, print and mail with it, so `scheduled-job:create` grants what a render needs, the way `scan2print:use` does for the scan station.
- A Purge Job has no owner at all, so whoever switches a cleanup step on has to hold the permission to delete what that step deletes. Turning on the history step needs the right to delete history records.

Deleting a schedule is not limited to its owner. Deleting only stops a schedule, it can never make one act with somebody's permissions.

<div style="border-left: 4px solid #F9C846; background: #fffbf0; padding: 12px 16px; margin: 16px 0; color: #525E5A;">
<strong>The Purge Job is deletable like any other schedule.</strong> With no active purge schedule, nothing cleans up history records, print tasks, mail tasks or orphaned files any more, and the disk keeps filling. The overview warns while that is the case. To bring the schedule back, use <strong>New</strong> and then <strong>Purge Job</strong> in the frontend, or run <code>php artisan scheduler:create-purge-job</code> on the server.
</div>

---

<h3 id="requirements" style="color: #203671; margin-top: 2.2em;">What has to be running</h3>

The Job Scheduler depends on the two background processes VeloxFactory runs in production.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Process</th>
      <th style="text-align: left; padding: 6px 10px;">Why</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Scheduler</td>
      <td style="padding: 6px 10px;">The minute tick that finds due schedules. Without it nothing fires at all.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Horizon</td>
      <td style="padding: 6px 10px;">The runs themselves. They have their own queue and their own worker, so a scheduled render that takes minutes never blocks a thumbnail or a mail.</td>
    </tr>
  </tbody>
</table>

Both belong under Supervisor in production. The Administration chapter has the details.

</div>

# Render Jobs: a render request with a clock attached

<div style="text-align: justify;">

A **Render Job** is a render request that VeloxFactory keeps and sends to itself. Whatever you can put into a call to the render endpoint goes into the schedule: parameters, data rows, the printer, the history record, the complete mailing segment. Nothing about rendering is special-cased for schedules, which is the point. A request you have tested once from the API behaves exactly the same at four in the morning.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scheduler.render-parameters.png</code><br>Parameters and data rows of a render job, with the placeholder switch active on a date parameter.
</div>

---

<h3 id="configure" style="color: #203671; margin-top: 2.2em;">What you configure</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Part</th>
      <th style="text-align: left; padding: 6px 10px;">What it decides</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Report Config</strong></td>
      <td style="padding: 6px 10px;">Which report is rendered. Exchangeable later in the frontend and through the API, and the payload is revalidated against the report it now points at.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>API token</strong></td>
      <td style="padding: 6px 10px;">Which of the owner's tokens the run authenticates with. Only tokens of the owner are accepted, and only ones that are neither revoked nor expired.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Output</strong></td>
      <td style="padding: 6px 10px;">What the render produces, and whether a history record is written.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Printing</strong></td>
      <td style="padding: 6px 10px;">Printer, number of copies and broadcast id, exactly as on the generate page.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Parameters and data rows</strong></td>
      <td style="padding: 6px 10px;">The values the report is rendered with, entered in the same table as on the generate page, here with placeholders allowed.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><strong>Mailing</strong></td>
      <td style="padding: 6px 10px;">The same block as everywhere else: mailer, template, recipients, attachments, background sending.</td>
    </tr>
  </tbody>
</table>

The **type** and the **owner** are the two things a schedule keeps for its whole life. A render job stays a render job, and it stays the user who created it, because that is whose permissions every run acts with.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>A schedule runs even when its owner cannot use the API.</strong> The scheduler calls the render internally and does not pass the API middleware, so <code>global:use-api</code> is not required for a schedule to fire. What does stop a run: a revoked or expired token, a token that no longer belongs to the owner, a deactivated owner, or a missing resource permission.
</div>

---

<h3 id="output" style="color: #203671; margin-top: 2.2em;">Output types</h3>

A schedule offers three of the four output types.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Type</th>
      <th style="text-align: left; padding: 6px 10px;">Use it when</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>url</code></td>
      <td style="padding: 6px 10px;">The usual choice. The PDF lands on the history disk, stays downloadable from the history record and is what a mail attaches.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>base64</code></td>
      <td style="padding: 6px 10px;">Same file, plus the document inside the history record. Comfortable, and noticeably larger per run.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>none</code></td>
      <td style="padding: 6px 10px;">Printing only. A print task is mandatory with it, the same rule the API applies.</td>
    </tr>
  </tbody>
</table>

`preview` is refused. A preview render deletes its own file again, so unattended it would produce nothing at all.

<div style="border-left: 4px solid #F9C846; background: #fffbf0; padding: 12px 16px; margin: 16px 0; color: #525E5A;">
<strong><code>none</code> and mailing do not go well together.</strong> Output type <code>none</code> produces no PDF, so a mailing that wants to attach one gets a mail without its attachment. Saving is not refused, the form says so as a note, and the honest combinations are <code>none</code> with <code>includePdf: false</code>, or <code>url</code>.
</div>

---

<h3 id="placeholders" style="color: #203671; margin-top: 2.2em;">Placeholders in parameters</h3>

A schedule that renders yesterday's figures every morning cannot have a fixed date in its parameters. Parameters and data rows therefore accept placeholders, resolved fresh at the start of every run.

Only those placeholders that exist *before* a render are offered here, so no `[data.first.*]` and no `[report.*]`: those describe a render that has not happened yet.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Placeholder</th>
      <th style="text-align: left; padding: 6px 10px;">Resolves to</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>[now.date]</code>, <code>[now.dateTime]</code>, <code>[now.time]</code></td>
      <td style="padding: 6px 10px;">The moment the run starts.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>[now.year]</code>, <code>[now.month]</code>, <code>[now.weekNumber]</code>, <code>[now.quarter]</code></td>
      <td style="padding: 6px 10px;">Period markers, for reports that select by week, month or quarter.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>[traceId]</code></td>
      <td style="padding: 6px 10px;">The trace id of this run, so the document can carry the id the log shows.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>[uuid]</code></td>
      <td style="padding: 6px 10px;">A fresh UUID, the same value everywhere it appears in one run and a new one in the next.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>[user.name]</code>, <code>[token.name]</code></td>
      <td style="padding: 6px 10px;">The owner of the schedule and the token it runs with.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>[env.hostname]</code>, <code>[appName]</code>, <code>[appUrl]</code></td>
      <td style="padding: 6px 10px;">Which instance produced the document.</td>
    </tr>
  </tbody>
</table>

The Placeholders button above the parameter table lists all of them with a description and an example, and copies a token with one click. `GET /scheduled-job/placeholders` returns the same catalogue.

<div style="border-left: 4px solid #F9C846; background: #fffbf0; padding: 12px 16px; margin: 16px 0; color: #525E5A;">
<strong>Typed fields need the switch.</strong> A date parameter is a date picker and a number parameter is a number field, and neither accepts <code>[now.date]</code> as text. The small button next to the field turns it into a text field for exactly that reason, and back again. Boolean parameters have no switch: a toggle has no text state, so they take no placeholders.
</div>

In the mailing block placeholders work as they do everywhere, and there the full catalogue applies: recipients, contact name and attachment names are resolved *after* the render, so `[data.first.customerEmail]` addresses the rows the report actually fetched.

---

<h3 id="validation" style="color: #203671; margin-top: 2.2em;">Checked when you save, not at four in the morning</h3>

A schedule that cannot possibly succeed is refused while you are still looking at the form.

- Every required parameter of the report has a value or a resource override, named individually if not.
- The owner holds the permissions the run will use: reading report configs, creating print tasks when a printer is configured, creating mail tasks when there is a mailing block.
- Mailer and mail template of the mailing block exist and are active.
- Every resource override path points inside the resources folder of the instance, the same check the render applies.
- The token belongs to the owner and is neither revoked nor expired.
- The cron expression is valid five field syntax, and a date for a single run lies in the future while you are setting or changing it.
- No `traceId` in the payload: every run makes its own, and a stored one would let the first run succeed and every one after it fail.

---

<h3 id="api" style="color: #203671; margin-top: 2.2em;">From the API</h3>

The schedule itself is ordinary master data. Its `payload` is the body you would have sent to the render endpoint.

```json
{
    "name": "Morning shift report",
    "type": "render",
    "cron": "0 6 * * 1-5",
    "timezone": "Europe/Berlin",
    "isActive": true,
    "reportConfig": "Shift Report",
    "token": 17,
    "errorMailer": "Office SMTP",
    "errorTo": ["ops@example.com"],
    "payload": {
        "outputType": "url",
        "parameters": { "P_DATE": "[now.date]", "P_RUN": "[uuid]" },
        "createPrintTask": false,
        "createHistoryRecord": true,
        "mailing": {
            "mailer": "Office SMTP",
            "mailTemplate": "Shift Report",
            "to": ["supervisor@example.com"],
            "includePdf": true,
            "pdfFileName": "Shift_[now.date]",
            "sendAsync": true
        }
    }
}
```

`reportConfig`, `errorMailer` and the mailer and template inside the payload accept either the numeric ID or the unique name, the same way the render endpoint does.

A job that is meant to happen once carries `runAt` in place of `cron`, and says with `deactivateAfterRun` whether it switches itself off afterwards:

```json
{
    "cron": null,
    "runAt": "2026-10-25 17:35",
    "deactivateAfterRun": true
}
```

Both fields come back on every read, `runAt` in the timezone of the schedule. Sending `cron` and `runAt` together is refused with 422, and `runAt: null` together with an expression turns the schedule back into a recurring one.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Endpoint</th>
      <th style="text-align: left; padding: 6px 10px;">Does</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /scheduled-job</code></td>
      <td style="padding: 6px 10px;">Lists schedules, filterable by <code>type</code> and <code>isActive</code>.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /scheduled-job/{id}</code></td>
      <td style="padding: 6px 10px;">A single schedule with its payload, its owner and its token.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /scheduled-job</code></td>
      <td style="padding: 6px 10px;">Creates one. The caller becomes the owner.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>PATCH /scheduled-job/{id}</code></td>
      <td style="padding: 6px 10px;">Changes it. Type and owner are refused, everything else you leave out keeps its stored value.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>DELETE /scheduled-job/{id}</code></td>
      <td style="padding: 6px 10px;">Deletes the schedule and its run log.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /scheduled-job/{id}/run</code></td>
      <td style="padding: 6px 10px;">Queues a run right now, with the owner's identity, not the caller's. A pending single run stays pending.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>POST /scheduled-job/preview-cron</code></td>
      <td style="padding: 6px 10px;">Validates an expression and answers with the next run times in a timezone.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /scheduled-job/placeholders</code></td>
      <td style="padding: 6px 10px;">The catalogue a schedule may use, for building your own form.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /scheduled-job-run</code></td>
      <td style="padding: 6px 10px;">The run log, filterable by schedule and status.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>GET /scheduled-job-run/{id}</code></td>
      <td style="padding: 6px 10px;">One run with its trace id, its result summary and its error message.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>DELETE /scheduled-job-run/{id}</code></td>
      <td style="padding: 6px 10px;">Deletes one run record. What the run produced is untouched.</td>
    </tr>
  </tbody>
</table>

Changing or starting a render schedule is limited to its owner and to administrators, because the run acts with the owner's permissions. Reading it, and reading its log, needs `scheduled-job:read` like any other lookup.

---

<h3 id="runnow" style="color: #203671; margin-top: 2.2em;">Trying it before you trust it</h3>

Leave the cron expression empty, save, and press **Run now**. The schedule never fires by itself, the run is recorded as a manual one, and you get the same result you would get at six in the morning: a history record, a print task if you configured a printer, a mail task if you configured mailing, and a line in the run log with the trace id that ties them together.

A schedule that is waiting for its single date can be tested the same way. **Run now** is a test run, not the appointment, so the date stays where it is.

When it does what you want, add the expression and switch the schedule on.

</div>

# The Purge Job: one schedule for all cleanup

<div style="text-align: justify;">

An instance that renders all day accumulates. History records with their PDFs, print tasks that have long been printed, mail tasks whose mails arrived weeks ago, and files on the history disk that no record points at any more. Somebody has to take that out, on a clock, without anybody thinking about it.

The **Purge Job** is that somebody. One schedule, one run, five entities, five retentions.

<div style="border: 2px dashed #b8bfcc; background: #f7f8fa; padding: 18px 16px; margin: 16px 0; text-align: center; color: #525E5A;">
<strong>Screenshot placeholder:</strong> <code>scheduler.purge-form.png</code><br>The purge settings with one retention field per entity and the switch that turns a step off.
</div>

---

<h3 id="steps" style="color: #203671; margin-top: 2.2em;">The five steps</h3>

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Step</th>
      <th style="text-align: left; padding: 6px 10px;">Deletes</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">1. Print Tasks</td>
      <td style="padding: 6px 10px;">Print tasks past the retention that have reached status <code>Printed</code>. One that never printed stays, so a problem does not disappear before anybody looked at it.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">2. Mail Tasks</td>
      <td style="padding: 6px 10px;">Mail tasks past the retention, whatever their status. The mail itself is long gone, the record is the receipt.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">3. History Records</td>
      <td style="padding: 6px 10px;">History records past the retention, together with their PDF and their xlsx export.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">4. Orphaned Files</td>
      <td style="padding: 6px 10px;">Files on the history disk that no history record and no mail task refers to any more, and that are older than the retention.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">5. Scheduler Run Log</td>
      <td style="padding: 6px 10px;">The run records of the Job Scheduler itself. The run doing the deleting never deletes itself.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #F9C846; background: #fffbf0; padding: 12px 16px; margin: 16px 0; color: #525E5A;">
<strong>The order is fixed and not configurable.</strong> Print tasks and mail tasks point at the history records they belong to, so the database refuses to delete a record while one of them still references it. Running the steps in this order is what makes the cleanup work, and it is the reason all five belong in one schedule instead of five.
</div>

---

<h3 id="retention" style="color: #203671; margin-top: 2.2em;">Saying how long</h3>

Every step has its own retention in days, and every step can be switched off on its own.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Value</th>
      <th style="text-align: left; padding: 6px 10px;">Means</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>90</code></td>
      <td style="padding: 6px 10px;">Delete what is older than 90 days.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>0</code></td>
      <td style="padding: 6px 10px;">Delete everything older than right now. Useful for a one-off clear-out, dangerous as a standing setting.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">switched off</td>
      <td style="padding: 6px 10px;">That entity is never purged. The step is named under <code>skipped</code> in the run result, so the log shows it was a decision and not an accident.</td>
    </tr>
  </tbody>
</table>

A sensible starting point keeps print and mail tasks shorter than the history records they belong to, and the orphaned files shortest of all. Something like 30, 30, 90, 7 and 90 days.

---

<h3 id="when" style="color: #203671; margin-top: 2.2em;">Saying when</h3>

A purge schedule says when it runs the same way every other schedule does: with a cron expression for the nightly housekeeping, or with a single date and time for a cleanup that belongs to one occasion, such as the clear-out after a migration. Both are on the *Job Scheduler* page.

A purge schedule that is waiting for its single date counts as active cleanup for as long as that date is still ahead, so the overview does not warn about missing housekeeping while one is armed. Once it has run, that cleanup is over; a *0* retention that was meant for exactly that one run does not stay armed for the next night.

---

<h3 id="setup" style="color: #203671; margin-top: 2.2em;">Where it comes from</h3>

The base setup of an instance creates the schedule under the name **Purge Job**, deactivated and without a cron expression. Its five retentions start from the `PURGE_*_DAYS` keys of the `.env` file, which are read once at that moment. From then on the retentions live on the schedule, and the `.env` keys are not read again.

Nothing is cleaned up until you give the schedule an expression and switch it on. That is deliberate: a fresh instance should not start deleting on a schedule nobody chose.

```
0 3 * * *     every night at 03:00
0 3 * * 0     Sunday nights only, for an instance with little traffic
```

---

<h3 id="permissions" style="color: #203671; margin-top: 2.2em;">Who may switch a step on</h3>

A Purge Job has no owner. It runs without any user at all, which means the usual permission check has nothing to check against. So the check moves to the moment a step is configured: whoever switches a step on, or changes its retention, has to hold the permission to delete what that step deletes.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Step</th>
      <th style="text-align: left; padding: 6px 10px;">Needs</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Print Tasks</td>
      <td style="padding: 6px 10px;"><code>report-print-task:delete</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">Mail Tasks</td>
      <td style="padding: 6px 10px;"><code>report-mail-task:delete</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;">History Records, Orphaned Files</td>
      <td style="padding: 6px 10px;"><code>report-history-record:delete</code></td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;">Scheduler Run Log</td>
      <td style="padding: 6px 10px;"><code>scheduled-job:delete</code></td>
    </tr>
  </tbody>
</table>

Steps you leave untouched are not asked about again, so somebody may change the cron expression without holding every delete permission in the instance.

---

<h3 id="result" style="color: #203671; margin-top: 2.2em;">What a run tells you</h3>

The run record carries the numbers per step: how many records were eligible, how many were deleted, how many could not be. A step that throws does not stop the ones after it.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Outcome</th>
      <th style="text-align: left; padding: 6px 10px;">Status</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Every configured step finished</td>
      <td style="padding: 6px 10px;"><code>Success</code></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Some steps finished, one failed or left records behind</td>
      <td style="padding: 6px 10px;"><code>Warning</code>, with the reason in the result</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;">Every configured step failed</td>
      <td style="padding: 6px 10px;"><code>Error</code>, and the failure mail goes out</td>
    </tr>
  </tbody>
</table>

A history record that could not be deleted because something still references it is logged individually, not swallowed into a count.

---

<h3 id="commands" style="color: #203671; margin-top: 2.2em;">Doing it by hand</h3>

The first four steps are each available as an artisan command as well, for a one-off cleanup or a maintenance window.

```bash
php artisan report-print-tasks:purge --days=30
php artisan report-mail-tasks:purge --days=30
php artisan report-history-records:purge --days=90
php artisan orphaned-files:purge --days=7
```

Run them in that order for the same reason the schedule does.

---

<h3 id="deleted" style="color: #203671; margin-top: 2.2em;">If the schedule is gone</h3>

The Purge Job is an ordinary schedule and can be deleted like any other. Nothing then cleans up, and the disk keeps filling. The Job Scheduler overview shows a warning for as long as no active purge schedule exists.

There are two ways back, and neither of them is special.

**From the frontend.** **New** on the Job Scheduler overview offers **Purge Job** next to Render Job. Pick it, set the retentions, give it a cron expression and switch it on. A purge schedule is created like any other schedule; it needs no owner and no API token, only a name that is still free and the delete permissions of the steps you switch on. The same way you would create a second one, for instance a weekly run with longer retentions next to a nightly one.

**From the command line**, for an instance you would rather not click through:

```bash
php artisan scheduler:create-purge-job
```

It creates the schedule under its original name when it is missing and does nothing when it is already there. It touches neither storage nor users, so it is safe on a live system. Afterwards give it its cron expression and switch it on again.

</div>

# Customer case studies

What customers built with VeloxFactory, and what it replaced.

# PPWR Compliance Documents at Scale

<div style="text-align: justify;">

<table style="width: 100%; border-collapse: collapse; margin-top: 1em;">
  <tbody>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; color: #525E5A; width: 160px;">Customer</td>
      <td style="padding: 6px 10px;"><a href="https://www.zeller-present.de" target="_blank"><strong>Zeller Present Handels GmbH</strong></a></td>
      <td rowspan="4" style="padding: 6px 10px; width: 130px; text-align: center; vertical-align: middle;"><a href="https://www.zeller-present.de" target="_blank"><img src="https://docs.veloxfactory.kiwi-software.dev/uploads/images/gallery/2026-09/embedded-image-6tkhrtt4.png" alt="Zeller Present Logo" style="width: 110px; height: auto;"></a></td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; color: #525E5A;">Industry</td>
      <td style="padding: 6px 10px;">Home and lifestyle products: kitchen, table, home &amp; living, bath, office, kids</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; color: #525E5A;">Project</td>
      <td style="padding: 6px 10px;">PPWR document generation with ERP integration</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; color: #525E5A;">Project duration</td>
      <td style="padding: 6px 10px;">approx. 4 weeks</td>
    </tr>
  </tbody>
</table>

<h3 id="overview" style="color: #203671; margin-top: 2.2em;">Overview</h3>

Zeller Present supplies home and lifestyle products for kitchen, table, home & living, bath, office and kids, sold through retail partners and its own online shop. The company has to comply with the EU Packaging and Packaging Waste Regulation (PPWR, [Regulation (EU) 2025/40](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32025R0040)). For every packaging it places on the market, the regulation requires an **EU declaration of conformity** and **technical documentation**. With an assortment of around **2,200 articles**, each shipped in up to three packaging levels and documented in two languages, writing these documents by hand was not an option.

As part of a project, **kiwi software** connected Zeller Present's ERP system Alphaplan 5 to VeloxFactory and developed a small middleware between the two systems. At the push of a button, or automatically once a month, VeloxFactory now produces **up to 12 compliant documents per article**, which are filed, versioned and published without any manual step.

---

<h3 id="the-challenge" style="color: #203671; margin-top: 2.2em;">The Challenge</h3>

The regulation turns every packaging level into its own documentation task. What Zeller Present needed:

- One declaration of conformity and one technical documentation per packaging level: individual packaging, outer carton and master carton
- Every document in German and English
- Around 2,200 articles with up to 12 documents each, up to roughly 26,000 documents in total
- Documents that stay in sync with ERP master data: when a dimension or a material weight changes, the affected documents must be reissued
- Previous versions retained for traceability
- The complete, current document set available for download by customers and authorities

---

<h3 id="the-solution" style="color: #203671; margin-top: 2.2em;">The Solution</h3>

kiwi software handled the ERP integration and built a lightweight middleware that sits between Alphaplan 5 and VeloxFactory. The middleware reads the packaging master data from the ERP, derives all document values and renders every document through VeloxFactory's REST API. The workflow:

1. The middleware selects all assortment articles that have no documents yet, or whose PPWR-relevant master data has changed since the last run, based on the ERP change log
2. For each article, it detects the packaging levels present: individual packaging, outer carton and master carton
3. It derives the values the regulation asks for: packaging type from the material weights, dimensions, weights, silica content, and the void ratio of outer and master cartons
4. For each packaging level, it calls VeloxFactory four times: declaration of conformity and technical documentation, each in German and English
5. The PDFs are filed on the document drive; any previous version is moved to a dated archive folder
6. The ERP receives a timestamp on the article, so it is only processed again when relevant data changes
7. Finally, one ZIP archive per language with the complete document set is uploaded to a public download server, and an info mail reports the result

The middleware runs the process automatically on the first day of every month and can be started at any time from the web interface. Since only new or changed articles are processed, an additional run is always safe.

---

<h3 id="technical-setup" style="color: #203671; margin-top: 2.2em;">Technical Setup</h3>

| Component | Details |
|---|---|
| Trigger | Scheduled job (monthly) and manual start from the web interface |
| ERP | Alphaplan 5 on MS SQL Server |
| Middleware | Lightweight Laravel middleware between ERP and VeloxFactory, developed by kiwi software as part of the project |
| Report engine | VeloxFactory, self-hosted on premises |
| Report templates | 4 Jaspersoft Studio templates (.jrxml): declaration of conformity and technical documentation, German and English |
| Data transfer | Parameters only, no SQL in the templates; up to 15 parameters per document |
| API call | `POST /report-config/{id}/render` with Base64 output, laconic response, no history record |
| Output | Up to 12 PDFs per article, versioned on the document drive, one ZIP archive per language |
| Volume | ~2,200 articles, up to ~26,000 documents |

Static content such as manufacturer details, signatory, legal references and the conformity assessment module lives in the report templates. Everything that varies per article and packaging level arrives as a parameter in the render request.

---

<h3 id="results" style="color: #203671; margin-top: 2.2em;">Results</h3>

The PPWR documentation for Zeller Present's entire assortment is now generated and maintained automatically:

- Up to 12 compliant documents per article, in German and English, without manual work
- Documents follow master data changes: only affected articles are regenerated
- Every previous version is archived with its date
- Consistent document numbers and layout across the whole assortment
- The public download archive is always complete and up to date

---

<h3 id="why-veloxfactory" style="color: #203671; margin-top: 2.2em;">Why VeloxFactory</h3>

VeloxFactory itself needed no changes for this project. The middleware, where the ERP knowledge lives, derives all values; VeloxFactory turns them into finished PDFs with one REST call per document. This kept the middleware small: no report engine, no PDF library and no layout code inside it.

Legal texts and layout sit in standard `.jrxml` templates, so wording can be updated without touching the middleware. For a run of this size, `laconicResponse` and disabled history records keep every response small and the database lean.

<p style="font-size: 0.9em; color: #525E5A;"><em>Published with the kind permission of Zeller Present Handels GmbH.</em></p>

</div>

# Administration

Running an instance: background processes, queues and maintenance.

# Background Job Processing

<div style="text-align: justify;">

VeloxFactory processes background work through a Redis-backed queue managed by <a href="https://laravel.com/docs/horizon" target="_blank">Laravel Horizon</a>: thumbnail generation, mail dispatch, and everything the Job Scheduler fires. Horizon runs as a single supervised process and manages its own worker pool internally, with dynamic scaling, real-time monitoring and a built-in dashboard.

---

<h3 style="color: #203671; margin-top: 2.2em;">Prerequisites</h3>

Horizon has two hard requirements beyond the base VeloxFactory stack: a running Redis instance and the `php-redis` PHP extension. Neither is optional, Horizon will refuse to start without both.

<h4 style="color: #203671; margin-top: 1.4em;">Redis</h4>

Redis can be installed natively or run as a Docker container. Both work equally well; the choice depends on your infrastructure preferences.

**Native installation:**

```bash
apt install redis-server
systemctl enable redis-server
systemctl start redis-server
```

**Docker container:**

```bash
docker run -d \
  --name redis \
  --restart unless-stopped \
  -p 127.0.0.1:6379:6379 \
  redis:alpine
```

The container binds to `127.0.0.1` only, Redis is not exposed to the network, which is the correct default for a single-server deployment.

Whichever method you choose, verify connectivity before proceeding:

```bash
redis-cli ping
# Expected: PONG
```

<h4 style="color: #203671; margin-top: 1.4em;">PHP Redis Extension</h4>

VeloxFactory is configured to use `phpredis` as the Redis client. The extension must be installed and active for PHP:

```bash
apt install php-redis
systemctl restart php8.2-fpm   # adjust version to match your PHP installation
```

Confirm the extension is loaded:

```bash
php -m | grep redis
# Expected: redis
```

<h4 style="color: #203671; margin-top: 1.4em;">.env Configuration</h4>

With Redis running and the extension installed, update your `.env` to activate Redis as the queue driver:

```dotenv
QUEUE_CONNECTION=redis

REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
```

---

<h3 style="color: #203671; margin-top: 2.2em;">Web Server</h3>

VeloxFactory requires a web server that routes all requests through Laravel's `public/index.php` entry point. Both Apache2 and nginx are supported. The Horizon dashboard at `/horizon` and the Reverb WebSocket endpoint require no special routing rules, they are handled by Laravel and PHP-FPM like any other request, with one exception: Reverb needs a WebSocket proxy pass.

<h4 style="color: #203671; margin-top: 1.4em;">Apache2</h4>

Enable `mod_rewrite` before configuring the vhost:

```bash
a2enmod rewrite proxy proxy_http proxy_wstunnel
systemctl restart apache2
```

Virtual host configuration:

```apache
<VirtualHost *:80>
    ServerName veloxfactory.example.com
    DocumentRoot /var/www/veloxfactory/public

    <Directory /var/www/veloxfactory/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    # Reverb WebSocket proxy
    ProxyPreserveHost On
    ProxyPass /app ws://127.0.0.1:8080/app
    ProxyPassReverse /app ws://127.0.0.1:8080/app

    ErrorLog ${APACHE_LOG_DIR}/veloxfactory-error.log
    CustomLog ${APACHE_LOG_DIR}/veloxfactory-access.log combined
</VirtualHost>
```

Laravel's bundled `.htaccess` in `public/` handles the rewrite rules, no additional configuration needed for URL routing.

<h4 style="color: #203671; margin-top: 1.4em;">nginx</h4>

```nginx
server {
    listen 80;
    server_name veloxfactory.example.com;
    root /var/www/veloxfactory/public;

    index index.php;

    # Laravel URL routing
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP-FPM
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_read_timeout 120;
    }

    # Reverb WebSocket proxy
    location /app {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
    }

    # Block dotfile access
    location ~ /\.(?!well-known).* {
        deny all;
    }
}
```

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0;">
ℹ️ <strong>Adjust the PHP-FPM socket path to match your PHP version.</strong> On systems with multiple PHP versions installed, the socket is typically at <code>/var/run/php/php8.2-fpm.sock</code>. Verify with <code>ls /var/run/php/</code>.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Queues</h3>

VeloxFactory uses three queues with distinct priorities:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Queue</th>
      <th style="text-align: left; padding: 6px 10px;">Jobs</th>
      <th style="text-align: left; padding: 6px 10px;">Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>high</code></td>
      <td style="padding: 6px 10px;">Thumbnail generation</td>
      <td style="padding: 6px 10px;">Dispatched on-demand when a report is rendered. Processed with highest priority, workers on this queue are never blocked by maintenance routines.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>default</code></td>
      <td style="padding: 6px 10px;">Mail dispatch, the scheduler tick</td>
      <td style="padding: 6px 10px;">Mails sent in background mode are queued here, together with the minute tick that looks for due schedules. Both are short jobs.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>scheduler</code></td>
      <td style="padding: 6px 10px;">Runs of the Job Scheduler</td>
      <td style="padding: 6px 10px;">Scheduled renders and the purge run. These are the long jobs of the instance, minutes rather than seconds, so they get a queue of their own.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0;">
ℹ️ <strong>The three queues run in separate worker pools.</strong> A purge run on the <code>scheduler</code> queue cannot delay thumbnail generation on <code>high</code> or a mail on <code>default</code>, they never compete for the same worker.
</div>

<h4 style="color: #203671; margin-top: 1.4em;">Mail Dispatch</h4>

A mailing sent with `sendAsync` creates its mail task immediately and hands the send to the `default` queue. The job carries only the ID of the task, never the attachments, so nothing large ever travels through Redis: the files are read from the history disk at send time.

A mail that runs into the dispatch rate limit of its mailer is not failed and not lost. The job is released back onto the queue with a delay until the next window opens, and the task stays *pending* in the meantime. Several workers may try to send at the same time, the limiter counts atomically in Redis, so the configured caps hold regardless of how many workers are running.

<h4 style="color: #203671; margin-top: 1.4em;">Scheduled Work</h4>

Every recurring job of the instance is a record in the Job Scheduler, with its own crontab expression. Laravel's own schedule holds exactly one entry: once a minute it queues `DispatchDueScheduledJobsJob`, which reads the due schedules from the database and queues one run per schedule on the `scheduler` queue.

That includes the purge run, which deletes print tasks, mail tasks, history records, orphaned files and the scheduler's own run log in one pass and in a fixed order. The *Schedule your jobs* chapter covers the retentions and the rest of the configuration.

A scheduled run is attempted once and is never retried: a second attempt would print a second label and send a second mail. Two runs of the same schedule never overlap either. What happened is on the run record, with its trace id.

---

<h3 style="color: #203671; margin-top: 2.2em;">Horizon Supervisors</h3>

Horizon manages workers through internal supervisors, process groups, each responsible for one queue. The system-level Supervisor (Supervisord) only ever manages the single Horizon master process; Horizon itself handles everything below that.

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px; white-space: nowrap;">Supervisor</th>
      <th style="text-align: left; padding: 6px 10px;">Queue</th>
      <th style="text-align: left; padding: 6px 10px;">Balancing</th>
      <th style="text-align: left; padding: 6px 10px;">Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>supervisor-rendering</code></td>
      <td style="padding: 6px 10px;"><code>high</code></td>
      <td style="padding: 6px 10px;">Auto</td>
      <td style="padding: 6px 10px;">Scales worker processes dynamically based on queue depth, up to 10 in production. Job timeout: 120 seconds.</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px; white-space: nowrap;"><code>supervisor-default</code></td>
      <td style="padding: 6px 10px;"><code>default</code></td>
      <td style="padding: 6px 10px;">Simple</td>
      <td style="padding: 6px 10px;">Fixed worker count, up to three processes in production. Runs with lower CPU priority (<code>nice 10</code>), mail dispatch should not compete with rendering for system resources. Job timeout: 300 seconds.</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px; white-space: nowrap;"><code>supervisor-scheduler</code></td>
      <td style="padding: 6px 10px;"><code>scheduler</code></td>
      <td style="padding: 6px 10px;">Simple</td>
      <td style="padding: 6px 10px;">Up to two processes, on its own queue connection <code>redis-scheduler</code> and with lower CPU priority (<code>nice 10</code>). Job timeout: 1800 seconds, long enough for a purge over a large history disk or a batch of scheduled renders.</td>
    </tr>
  </tbody>
</table>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0;">
ℹ️ <strong>A connection's <code>retry_after</code> has to stay above the job timeout of every supervisor using it.</strong> A job that runs longer than <code>retry_after</code> is considered lost and handed to a second worker while the first is still working on it, which for a scheduled render means a duplicate print and a duplicate mail. The <code>redis</code> connection allows 360 seconds against a timeout of 300, <code>redis-scheduler</code> allows 1860 against 1800. Raise <code>retry_after</code> first whenever you raise a timeout.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">System Supervisor Configuration</h3>

Supervisord keeps three VeloxFactory processes alive and restarts them automatically on failure: the Horizon master, the Reverb WebSocket server, and the scheduler.

```ini
; Horizon - manages all VeloxFactory queue workers internally
[program:veloxfactory-horizon]
directory=/var/www/veloxfactory
command=/usr/bin/php artisan horizon
user=www-data
process_name=%(program_name)s
numprocs=1
autostart=true
autorestart=true
startretries=10
stopasgroup=true
killasgroup=true
stopsignal=TERM
startsecs=3
stopwaitsecs=1860
redirect_stderr=true
stdout_logfile=/var/log/supervisor/veloxfactory-horizon.log
environment=HOME="/home/www-data",PATH="/usr/local/bin:/usr/bin:/bin"

; Reverb - WebSocket server for real-time events
[program:veloxfactory-reverb]
directory=/var/www/veloxfactory
command=/usr/bin/php artisan reverb:start
user=www-data
autostart=true
autorestart=true
startretries=10
stopasgroup=true
killasgroup=true
stopsignal=INT
startsecs=3
stopwaitsecs=60
redirect_stderr=true
stdout_logfile=/var/log/supervisor/veloxfactory-reverb.log
environment=HOME="/home/www-data",PATH="/usr/local/bin:/usr/bin:/bin"

; Scheduler - runs Laravel's task schedule every minute
[program:veloxfactory-schedule]
directory=/var/www/veloxfactory
command=/usr/bin/php artisan schedule:work
user=www-data
autostart=true
autorestart=true
startretries=10
stopasgroup=true
killasgroup=true
stopsignal=INT
startsecs=3
stopwaitsecs=60
redirect_stderr=true
stdout_logfile=/var/log/supervisor/veloxfactory-schedule.log
environment=HOME="/home/www-data",PATH="/usr/local/bin:/usr/bin:/bin"
```

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0;">
ℹ️ <strong><code>stopsignal=TERM</code> is required for Horizon.</strong> SIGTERM triggers a graceful shutdown, Horizon finishes any in-flight jobs before stopping its workers. Using SIGKILL or SIGINT instead will interrupt running jobs mid-execution and may leave Report History Records in an incomplete state.
</div>

<div style="border-left: 4px solid #F9C846; background: #fffbf0; padding: 12px 16px; margin: 16px 0; color: #525E5A;">
<strong>The scheduler process drives every recurring job of the instance.</strong> Without <code>schedule:work</code> running (or an equivalent cron entry calling <code>schedule:run</code> every minute), no schedule fires: no scheduled render, no cleanup, and the disk keeps filling. A cron entry is the alternative on hosts without Supervisord:<br><code>* * * * * cd /var/www/veloxfactory &amp;&amp; php artisan schedule:run &gt;&gt; /dev/null 2&gt;&amp;1</code>
</div>

After updating the configuration:

```bash
supervisorctl reread
supervisorctl update
supervisorctl status
```

---

<h3 style="color: #203671; margin-top: 2.2em;">The Dashboard</h3>

The Horizon dashboard is available at `/horizon`. It is restricted to users with the `global:admin` permission, the same permission required to manage users and system-wide settings.

The dashboard provides a real-time view of the entire queue system: pending and completed jobs, throughput metrics, worker counts per supervisor, and a full log of failed jobs with their stack traces. Failed jobs can be retried directly from the dashboard without any CLI access.

For scheduled work the Job Scheduler's own run log is the better place to look: it holds one record per run with its trace id, its result and the history record it produced. Horizon shows the job, the run log shows what the job did.

---

<h3 style="color: #203671; margin-top: 2.2em;">Graceful Shutdown &amp; Deployments</h3>

When Horizon is stopped, for deployments, configuration changes, or server maintenance, it finishes any jobs currently in flight before exiting. The Supervisord configuration allows up to 1860 seconds for this drain, matching the longest job timeout of any supervisor, so even a purge run over a large history disk finishes cleanly.

```bash
# Stop Horizon gracefully (in-flight jobs will finish first)
supervisorctl stop veloxfactory-horizon

# Restart after a deployment
supervisorctl restart veloxfactory-horizon
```

Never force-kill the Horizon process during a deployment. Always use `supervisorctl stop` or `supervisorctl restart`, both send SIGTERM and wait for the drain period.

A deployment window shorter than the drain period is worth planning around the schedules: a restart during the nightly purge waits for it, and a schedule whose slot falls into the downtime is dropped rather than fired late.

</div>

# Integrations

Connecting VeloxFactory to other systems and to AI assistants.

# AI MCP Server

<div style="text-align: justify;">

VeloxFactory can be operated directly through an AI assistant. A dedicated **MCP Server**, available as an add-on for VeloxFactory customers, exposes report configs, rendering, print dispatching, mailing, recurring jobs and user administration through the Model Context Protocol (MCP), so any MCP-compatible AI assistant can manage VeloxFactory through plain conversation instead of the frontend or hand-written API calls.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
💬 <strong>In short:</strong> if you can describe the task, your assistant can do it. No API documentation to read, no frontend clicks to remember, and not tied to any single AI provider.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">What It Lets You Do</h3>

The MCP Server gives your assistant access to everything VeloxFactory can do, organised into a few practical areas.

**Manage reports by talking, not clicking.** Create, update, and organise report configs, connection configs, and report contexts through conversation. "Clone the KanBan label as a QA version and rename the fields" is a request your assistant can carry out directly.

**Render and preview on demand.** Ask for any report to be rendered with real or example data, and get a finished PDF back, opened automatically, ready to view. No need to log into the frontend or assemble an API request by hand. This includes swapping an image in on the fly for a single render, a file from the instance's resources folder, an `https://` URL, or a Base64 upload, without editing the report configuration itself.

**Handle files without the round-trip.** Uploading a new `.jrxml` template, swapping a logo, or attaching a resource image is as simple as pointing your assistant at the file on your computer. It takes care of the technical side.

**Dispatch print jobs.** Have a rendered document sent straight to a print task, check its status, or mark it as printed, the same dispatching VeloxFactory already does, just requested in a sentence. Printers can be named the way people name them: VeloxFactory resolves a display name such as *Warehouse Label 01* to the queue name of the print server and fills in the broadcast channel by itself, so the assistant never has to look a printer up first.

**Maintain the printer master data.** List, create, update, retire, or delete the printers your company prints on, including location, type and broadcast ID, so the pickers in the frontend and every print task stay in sync with reality.

**Send a rendering by mail.** A render can go out as a mail in the same request, with the PDF and an optional xlsx attached. Mailers and mail templates are referenced by name, so *"render the delivery note for order 4711 and mail it to dispatch"* is one message. The assistant can also set the mail side up from scratch: create an SMTP account from a provider's documented settings, send a test mail to prove the credentials, write a template against the real placeholder catalogue of a specific report, and preview it before anyone receives it. A document that was rendered earlier can be mailed again at any time, to a freely chosen recipient.

**Put recurring work on a schedule.** The shift report that belongs in a mailbox at six every morning does not need anyone to ask for it. The assistant can turn a sentence into a schedule, confirm the crontab expression by reading the next run times back, run the schedule once by hand to check it, and only then switch it on. A schedule can just as well be pointed at a single date and time instead of a crontab expression, firing exactly once and, if wanted, deactivating itself afterwards. The cleanup schedule that keeps history, print tasks, mail tasks and files from growing without bound is maintained the same way, one retention at a time.

**Administer users and access.** Create accounts, adjust permissions, reset passwords and enable or disable access. Day-to-day administration happens in conversation instead of in the interface.

**Keep track of history.** Pull up past renderings, review what was generated when and by whom, follow a trace id from a scheduled run through to the mail it produced, and clean up records that are no longer needed.

<h4 style="color: #203671; margin-top: 1.8em;">At a Glance</h4>

Coverage is complete, every part of VeloxFactory is reachable, not just the rendering side:

<table style="width: 100%; border-collapse: collapse;">
  <thead>
    <tr style="border-top: 1px solid #e6e8ef; border-bottom: 1px solid #e6e8ef;">
      <th style="text-align: left; padding: 6px 10px;">Area</th>
      <th style="text-align: left; padding: 6px 10px;">What's covered</th>
    </tr>
  </thead>
  <tbody>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Users</td>
      <td style="padding: 6px 10px;">List, view, create and update accounts; change passwords; enable and disable access</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Report Contexts</td>
      <td style="padding: 6px 10px;">List, view, create, update, and delete</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Connection Configs</td>
      <td style="padding: 6px 10px;">List, view, create, update, delete, and test a database connection</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Common Report Resources</td>
      <td style="padding: 6px 10px;">List, view, create, update, and delete (logos, images, shared files)</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Report Configs</td>
      <td style="padding: 6px 10px;">List, view, create, update, delete, manage resources/parameters/fields, generate previews, and render (including per-render image overrides via <code>resourceOverrides</code> and an optional <code>mailing</code> segment)</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Report Resources</td>
      <td style="padding: 6px 10px;">Link or unlink a Common Report Resource to a report</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">History Records</td>
      <td style="padding: 6px 10px;">List, view, create, update, delete, print, mail again, and clean up linked print tasks</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Print Tasks</td>
      <td style="padding: 6px 10px;">List, view, create, update, delete, and set status/printed state</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Printers</td>
      <td style="padding: 6px 10px;">List, view, create, update, and delete printer master data; names are resolved automatically when a print task is dispatched</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Mailers</td>
      <td style="padding: 6px 10px;">List, view, create, update, and delete SMTP accounts, and send a test mail to prove the credentials</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Mail Templates</td>
      <td style="padding: 6px 10px;">List, view, create, update, and delete; read the placeholder catalogue of a report, and preview subject and body without sending anything</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Mail Queue</td>
      <td style="padding: 6px 10px;">List and view mail tasks, mail an existing rendering, create a task directly, repeat one with corrected recipients, and delete</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Scheduled Jobs</td>
      <td style="padding: 6px 10px;">List, view, create, update, and delete schedules; validate a crontab expression and preview its next run times; set a schedule to run once at a given date and time instead of recurring; read the placeholders a schedule may use; run one immediately; adjust the retentions of the cleanup schedule</td>
    </tr>
    <tr style="border-bottom: 1px solid #e6e8ef;">
      <td style="padding: 6px 10px;">Run Log</td>
      <td style="padding: 6px 10px;">List and view the runs of a schedule, searchable by trace id and outcome, and delete single entries</td>
    </tr>
    <tr>
      <td style="padding: 6px 10px;">Local Utilities</td>
      <td style="padding: 6px 10px;">Automatically open rendered PDFs, URLs, and output folders on your machine</td>
    </tr>
  </tbody>
</table>

<p style="margin-top: 0.8em;">89 tools in total, covering the full VeloxFactory API.</p>

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>API tokens are not part of the tool set.</strong> They are issued and revoked in the web interface only. The MCP server talks to VeloxFactory with a token of its own, and a request authenticated that way cannot manage tokens. Asked for a token, the assistant points at <strong>Users → user → API Tokens</strong> instead.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">What This Looks Like in Practice</h3>

A few examples of the kind of request the MCP Server is built for:

- "Render the A5 KanBan label for article 4561287-154 and show me the PDF."
- "I designed a new label in Jaspersoft Studio, onboard it as a report config."
- "Create a new user for our warehouse lead with admin rights."
- "Print this delivery note to the shop floor printer and let me know once it's done."
- "Show me every report config connected to the QA database."
- "Render this label but use this product photo instead of the linked one, just for this one print."
- "Add the new label printer in hall 2 and send the next delivery note there."
- "Mail this morning's shift report to the supervisor again, this time with the Excel export."
- "Set up our Office 365 account as a mailer and send a test mail to check it works."
- "Send the weekly stock list to disposition every Monday at six, and tell me the next three times it would run."
- "Run the year-end archive job once on New Year's Eve at midnight and turn the schedule off afterwards."
- "Which scheduled runs failed last night, and what did they say?"

Each of these would otherwise mean a frontend session or a small API script. With the MCP Server, they are a single message.

---

<h3 style="color: #203671; margin-top: 2.2em;">Built-In Know-How</h3>

Access to the tools is only half the story. Alongside the MCP Server, kiwi software also provides a companion **Skill**, a packaged layer of accumulated know-how that teaches the assistant how to use those tools well, not just that they exist.

In practice, this is what separates "the assistant can technically call the API" from "the assistant makes the same good judgment calls a person who knows VeloxFactory inside out would make": choosing the right output format for the situation, batching multiple renders sensibly instead of opening five windows in a row, staying within an instance's rate limit, and following VeloxFactory's own conventions rather than generic assumptions.

It also covers the judgment calls that only matter once something runs unattended: checking what a render actually reported about its mail instead of trusting a 200, and testing a schedule by hand before giving it a cron expression, so nobody finds out at three in the morning that it prints four hundred labels.

The most visible example: with the Skill in place, the assistant can **design a brand-new report template from scratch** (a label, delivery note, or certificate) without ever opening Jaspersoft Studio. It knows the exact conventions a `.jrxml` file needs to work in VeloxFactory (parameter naming, example values, and so on) and checks its own work with a preview before calling the template done.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
💬 The Skill ships together with the MCP Server as part of the same add-on, there is nothing separate to install or configure.
</div>

---

<h3 style="color: #203671; margin-top: 2.2em;">Who It Is For</h3>

The MCP Server is aimed at teams that already run VeloxFactory and want a faster way to handle the routine work around it, onboarding new report templates, day-to-day administration, one-off renderings, or quick checks on what has already been generated.

It is a natural fit for anyone comfortable working with an AI assistant who would rather describe a task than click through a frontend or write an API call, including teams without dedicated developer resources to spare on integration work. Since MCP is an open, provider-independent standard, it is not limited to a single AI product.

---

<h3 style="color: #203671; margin-top: 2.2em;">Getting It</h3>

The MCP Server is built and maintained by kiwi software as an add-on to a VeloxFactory license. It runs locally alongside your MCP-compatible AI assistant (Claude being one example) and connects to your VeloxFactory instance using your own API credentials. Nothing about your data passes through a third party.

Setup is handled together with kiwi software as part of onboarding, typically alongside the initial VeloxFactory rollout or added to an existing installation.

<div style="border-left: 4px solid #5fc75d; background: #f6fdf6; padding: 10px 16px; margin: 16px 0; border-radius: 0 4px 4px 0;">
ℹ️ <strong>Interested?</strong> Reach out via the <a href="/books/veloxfactory/page/get-veloxfactory">Get VeloxFactory</a> page, or try the live demo first at <a href="https://demo.veloxfactory.dev" target="_blank">demo.veloxfactory.dev</a>.
</div>

</div>

# Project Examples

Typical VeloxFactory use cases from real-world projects.

# Article Label Printing at Scale

<div style="text-align: justify;">

<h3 id="overview" style="color: #203671; margin-top: 2.2em;">Overview</h3>

A trading company with its own in-house logistics operation needed a fast, reliable way to generate article labels directly from their ERP data. Manual label creation had become a bottleneck as order volumes grew, and the team was looking for a solution that would integrate cleanly with their existing infrastructure without adding complexity.

Together with **kiwi software**, they rolled out VeloxFactory as the rendering engine behind a lean scanning workflow that now produces over **4,000 labels per day**, fully automated and delivered straight to the warehouse floor.

---

<h3 id="the-challenge" style="color: #203671; margin-top: 2.2em;">The Challenge</h3>

Warehouse staff had been looking up article data manually, copying it into label templates, and printing. As the business grew, this process couldn't keep up. What they needed was simple:

- Labels generated in under a second per scan
- Data pulled directly from the ERP, no manual input
- An interface that works with a standard barcode scanner, no keyboard required
- No additional middleware, no fat clients, no complex integrations

---

<h3 id="the-solution" style="color: #203671; margin-top: 2.2em;">The Solution</h3>

kiwi software built a lightweight single-page scanning mask that ties directly into VeloxFactory's REST API. The whole frontend was finished in a single day. The workflow is straightforward:

1. A warehouse employee scans an article barcode
2. The scanning mask sends one API request to VeloxFactory, passing only the article number
3. VeloxFactory fetches all relevant article data in real time from the connected MS SQL database
4. The Jaspersoft report template is rendered with the live data and output as a print-ready PDF
5. VeloxFactory dispatches the label to the company's print server, which routes it to the nearest floor printer

Scan in, label out. Typically within 800 ms end-to-end.

---

<h3 id="technical-setup" style="color: #203671; margin-top: 2.2em;">Technical Setup</h3>

| Component | Details |
|---|---|
| Frontend | Custom scanning mask (developed by kiwi software) |
| Report engine | VeloxFactory |
| Report template | Jaspersoft Studio (.jrxml), designed to label specs |
| Data source | MS SQL Server via VeloxFactory data adapter (sqlsrv driver) |
| API input | Article number only |
| Print delivery | VeloxFactory Print Task to on-premises print server |
| Daily volume | 4,000+ labels |

The data adapter resolves the article number into the full set of label attributes: description, unit, weight class, hazard indicators and storage zone. The report template then lays these out according to the company's label specification.

---

<h3 id="results" style="color: #203671; margin-top: 2.2em;">Results</h3>

Since going live, the process runs without any manual intervention:

- 4,000+ labels generated and printed per day, fully automated
- Label generation time down from several minutes to under one second
- Data entry errors eliminated completely
- Staff need no training beyond a single walkthrough of the scanning mask
- No local print drivers or client software required on scanning stations

---

<h3 id="why-veloxfactory" style="color: #203671; margin-top: 2.2em;">Why VeloxFactory</h3>

The combination of a clean REST API, native data adapter support and built-in print server integration meant there was no need for a separate middleware layer or custom rendering pipeline. kiwi software connected the MS SQL database via the sqlsrv driver, configured the print server target and had the scanning frontend running in a day.

> "We connected the database, set up the print target and had a working frontend the same day."
> -- kiwi software, project lead

</div>

# Bill of Materials Generation

<div style="text-align: justify;">

<h3 id="overview" style="color: #203671; margin-top: 2.2em;">Overview</h3>

A manufacturing company needed a simple, reliable way to get printed bills of materials into the hands of their foremen at the start of each production run. Their existing process involved exporting data manually from their production system, which was slow and prone to errors when order volumes picked up.

By connecting VeloxFactory to their MySQL production database, they now generate 200 to 300 bills of materials per day, each one triggered with nothing more than a production order reference number.

---

<h3 id="the-challenge" style="color: #203671; margin-top: 2.2em;">The Challenge</h3>

Before VeloxFactory, generating a bill of materials meant pulling data out of the production system by hand, formatting it and sending it to print. The foreman often had to wait, and mistakes in the data transfer occasionally caused issues on the shop floor. The company needed:

- A fast way to generate accurate, print-ready BOMs from live production data
- A process simple enough for office staff to use without training
- No dependency on exports or manual data handling

---

<h3 id="the-solution" style="color: #203671; margin-top: 2.2em;">The Solution</h3>

kiwi software connected VeloxFactory to the company's MySQL production database using a configured data adapter. Staff now use VeloxFactory's built-in web frontend to trigger BOM generation: they type in the reference number of a production order, VeloxFactory fetches the full set of production data from the database and renders a structured bill of materials as a PDF. The document is printed and handed to the foreman before the run starts.

No exports, no formatting, no waiting.

---

<h3 id="technical-setup" style="color: #203671; margin-top: 2.2em;">Technical Setup</h3>

| Component | Details |
|---|---|
| Frontend | VeloxFactory web frontend (built-in) |
| Report engine | VeloxFactory |
| Report template | Jaspersoft Studio (.jrxml) |
| Data source | MySQL database via VeloxFactory data adapter |
| API input | Production order reference number |
| Output | Print-ready PDF bill of materials |
| Daily volume | 200 to 300 BOMs |

The data adapter resolves the production order reference into the full component list, quantities, units and any production notes required for the BOM layout.

---

<h3 id="results" style="color: #203671; margin-top: 2.2em;">Results</h3>

Since the rollout, BOM generation is no longer a bottleneck before production starts:

- 200 to 300 bills of materials generated per day
- Generation time reduced from several minutes to seconds
- No more manual data exports or formatting steps
- Foremen receive accurate, consistently formatted documents every time

---

<h3 id="why-veloxfactory" style="color: #203671; margin-top: 2.2em;">Why VeloxFactory</h3>

VeloxFactory's built-in web frontend meant no custom tooling was needed at all on the user side. kiwi software set up the MySQL data adapter and the Jaspersoft report template, and the company was up and running within a day. The foremen now get a clean, standardised BOM for every production order without anyone having to touch the underlying data.

> "Our foremen get the BOM before the run starts. That used to take minutes, now it takes seconds."
> -- Production manager

</div>