Case study

BNN Label Automation — Zero-Touch Label Printing from Slack

  • Role Designed and built
  • Client Joe Marketplace · 2024–2025
  • Stack n8n, Slack, Flask + Pillow, pywin32, WireGuard, NSSM
  • Status Live production

n8n Slack Flask Pillow pywin32 WireGuard NSSM Windows 11 Brother PT-D610BT

11

n8n workflow nodes

< 3 s

Slack response time

10

Max labels per request

24/7

Always-on service

The problem

Devices in the Joe Marketplace fleet need a printed label with model and serial number before they ship or return to a store. Printing one used to mean finding the one machine with the Brother driver installed, or fighting a shared printer nobody trusted. Non-technical team members should never have to touch a printer driver or a configuration file.

Goal: from a Slack command to a physical label in seconds, from any laptop, with no drivers, no shared printers and no per-machine setup.

How it works

01 · Slack slash command

Team members type /print-label in the #joe-label-device channel. Slack is the tool the team already uses, so there is nothing new to learn.

02 · n8n instant acknowledgement

The webhook fires and a respondToWebhook node answers inside Slack’s three-second window, preventing the “app did not respond” timeout. Processing continues asynchronously.

03 · Label server

A Flask + Pillow + pywin32 server on a headless Windows 11 PC generates a pixel-perfect bitmap and sends it natively to the Brother PT-D610BT. No PowerShell, no corrupt 60-byte jobs.

04 · WireGuard VPN

n8n on the cloud VPS reaches the on-premises PC through an encrypted tunnel on UDP 51820. The PC is invisible to the public internet: no port forwarding, no exposure.

05 · Confirmation and visibility

After printing, a public message posts to the channel with the device model, serial number and quantity, so everyone can see what was printed and when.

The pipeline, step by step

  • TriggerSlack /print-label [model] [serial] [qty]
  • AcknowledgerespondToWebhook → “Label request received…”
  • ValidateCheck the model, parse the serial, clamp the quantity to 1–10
  • RouteHTTP POST to the label server on port 5678 over WireGuard
  • PrintFlask → Pillow bitmap → pywin32 → Brother PT-D610BT
  • DonePost the public confirmation to #joe-label-device

The tools

n8n · automation backbone

Self-hosted at n8n.richardapplegate.io. Orchestrates the entire flow from Slack webhook to print confirmation in 11 nodes.

Slack · team interface

The “Esper Device Label” app provides the /print-label command. Nobody needs a printer driver.

Flask + Pillow · label server

label_server.py generates label bitmaps with pixel-level control over DPI and dimensions, which fixed the corrupt 60-byte jobs PowerShell produced.

pywin32 · native print API

Direct Windows GDI print calls. Reads LOGPIXELSX from the device context to get the true DPI at runtime, avoiding a 900 px to 682 px clipping bug.

WireGuard · encrypted tunnel

VPS-to-PC tunnel over UDP 51820. The label PC stays off the public internet while remaining reachable from n8n.

NSSM · service manager

Wraps the Flask server as a proper Windows service that survives reboots. The headless PC auto-logs in via Sysinternals Autologon with sleep and lock disabled.

Infrastructure

The system splits cleanly into two environments: a cloud VPS running the full Docker-based self-hosted stack, and an on-premises Windows PC dedicated exclusively to label printing. WireGuard bridges the two.

Team

Slack · /print-label · #joe-label-device

Cloud VPS · Docker

n8n, Grafana, Portainer, RustDesk Pro · WireGuard 10.8.0.1

On-prem · Windows 11

label_server.py · Flask on port 5678 · NSSM service · 10.8.0.16

Hardware

Brother PT-D610BT over USB

Because the automation surface never touches the hardware directly, any future printer, Brother, Zebra or otherwise, can be added at the Windows layer without changing n8n or Slack. RustDesk Pro provides headless remote access for maintenance, so no monitor or keyboard is ever plugged into the label PC once it is running.

Lessons learned

n8n IF node bug

Any non-empty string is true. In n8n v2.2, even "false" or "no" evaluates as boolean true, so IF nodes are unreliable for string branching. Code nodes that return [] halt execution on the unwanted path instead.

Slack’s three-second window

Acknowledge first, work second. Slack drops slash-command connections after three seconds. The acknowledgement node must fire before any HTTP calls or validation; the real work continues in parallel afterwards.

pywin32 over PowerShell

Native GDI printing fixed corrupt output. PowerShell print jobs produced 60-byte garbage. Reading GetDeviceCaps(8) after creating the device context returns the real runtime DPI and prevents bitmap clipping.

Webhook auth side effects

Header auth leaks into triggers. Setting httpHeaderAuth on HTTP Request nodes silently made the Webhook trigger require auth too, which blocked Grafana. Every Webhook trigger now sets authentication: 'none' explicitly.