Print agents
Updated
The office print dialog has two destinations. One is the browser
(window.print()). Print agents are the other — a small native
host on Windows or Linux that signs into the tenant as a person, ticks the
printers on that machine, and prints whatever the spooler can take.
This is platform infrastructure, not a workspace business model. Do not revive
MIQS KERNEL.PRINTERS, PrintServer, PrintQueueTask, or Zyan IPrintService.
Those tables are not on the tenant EF schema.
Why a separate process#
The tenant cannot see a USB label printer in a warehouse. The agent can. The
split is the same as the Android app: the platform owns the queue and the ACL;
the host is a native binary in zulo-print-agent. The agent does not load ERP
documents, prices, or metadata. It hosts printers and prints blobs.
Why a person signs in (not a pairing code)#
The host of the machine is a user. They tick printers; those printers appear in the back office. Jobs still carry the office user who clicked Print, so the queue tells you who asked, not only which PC printed.
The agent uses POST /api/auth/login and refresh, the same tokens as the phone.
There is no machine token.
Why the Windows service is not LocalSystem#
LocalSystem does not see the signed-in user's printers (network shares, USB
drivers installed per-user). The tray installs the service to run as that
Windows account (or a dedicated print user). If sc config cannot set the
password, a logon scheduled task starts the same exe.
Tables (tenant EF, schema sync)#
| Table | Role |
|---|---|
PrintAgents |
Machine, OS, last seen, signed-in host user, Online/Offline/Stopped |
PrintAgentPrinters |
OS sysname, display name, Enabled (the checkbox), online |
PrintJobs |
Printer, requested-by user, content type, blob, copies, state |
RolePrinterAccess |
Role × printer, grant or revoke |
UserPrinterAccess |
Extra ticks on the user card |
An unchecked printer is disabled (and deleted only when no jobs remain). It does not vanish from history.
API#
All JWT.
POST /api/print/agents/register— upsert bymachineIdstored on diskPUT /api/print/agents/me— heartbeatPUT /api/print/agents/me/printers— the checkbox listGET /api/print/agents/me/jobs?machineId=&waitSeconds=— long-poll, thenGET …/jobs/{id}/contentandPATCHPrinting/Done/ErrorGET /api/print/printers— catalog (Admin)GET /api/print/printers/available— printers the current office user may usePOST /api/print/jobs—{ printerId, copies }plus either{ printFormMetaId, recordId }(server renders PDF viaIDocumentPdfService) or{ contentType, bytes }(Base64)GET /api/print/jobs+PATCHHold/Cancel/Queued +POST …/reprint— requires named permissionprint.queue.manage, not “anyone who can print”
ACL#
Enqueue requires access to that printer. A revoke on any of the user's own roles wins over grants and over extra ticks on the user. Superuser / Admin see every enabled printer. Agent pull requires the signed-in user to own that agent.
Script seam#
var queue = GetService<IPrintQueueService>();
await queue.EnqueuePrintFormAsync(printerId, printFormMetaId, documentId, copies: 1);
await queue.EnqueueFileAsync(printerId, "application/vnd.hp-pcl", bytes, copies: 1);
When a user is on the call, ACL still applies. A background job with no principal may enqueue onto any enabled printer. Word/Excel are not converted on the server; if the machine has a driver that accepts them, the raw file still goes through.
Trap#
MetaAgent is the job scheduler agent (enroll key, tags, run-as). Print
agents are PrintAgent. Mixing the two in the UI or in a script will look like
the printer catalog is empty while jobs still run on a warehouse box.
Do not assign HttpClient.BaseAddress after the first request — .NET throws
"Properties can only be modified before sending the first request". Sign-in
then register used the same client and hit that. Each call uses an absolute
URI from the tenant name instead.
A dialog You must install .NET Desktop Runtime means the process is a
framework-dependent apphost (dotnet build output, or an exe copied out of
the folder). The installed SDK is not enough — the apphost wants
Microsoft.WindowsDesktop.App at the exact patch the CI SDK used. The
shipping zip is two self-contained single-file exes; they do not look for a
machine runtime.
A Windows service set to this user without a password never starts
(Cannot start service 'ZuloPrintAgent'). LocalSystem starts but does not see
USB printers. Install service registers a logon scheduled task and starts
zulo-print-agent.exe in the current session.
The Windows download is the zip, not the MSI. CI still builds an unsigned
MSI, but Chrome marks that file as “not commonly downloaded”. Until the MSI is
Authenticode-signed, only zulo-print-agent-win-x64.zip is offered. Unzip and
run zulo-print-agent-tray.exe; Install service on the tray registers the
Windows service.
The download links live in the customer cabinet (getzulo.com/cabinet →
Apps), not on the marketing site. The agent asks for a tenant, a user and a
password on its first screen, so a visitor who is not signed in cannot use the
file — the install steps name their own stand instead of acme. The release
assets themselves are the same zip CI publishes.
Same topic for other readers: User