

Administrator
Options
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
an hour ago
Check out other articles in the C3 Series
Introduction: Cloud Meets Physical Hardware
How the heck do you get cloud software to spit out physically printed cards at a conference booth? That was the question we ran into when we decided to print CreatorCon C3 cards at Knowledge 25 for attendees. And we came up with a few possible solutions, but we couldn't shake our desire to do as much as possible on ServiceNow, so we turned to our friend behind the firewall: the MID Server. But honestly, that is a vast oversimplification of the solution that ultimately delivered the much beloved cards into the real world.
Our solution involved orchestrating a complex dance across virtual MID servers and their hosts using MID servers, Java file operations, a third-party hot folder utility, and the most important part:
All the amazing folks who cut the cards to size, kept the printers stocked with paper, stuffed cards in plastic sleeves, and organized them on the booth for pickup.
Wow. Even the intro was complicated. With that, let's dive into how we built a system that bridged cloud software with physical hardware, and what we learned about MID server integrations along the way.
The Challenge: Printing from the Cloud
ServiceNow lives in the cloud. Printers live on conference floors. Getting them to talk requires bridging that gap, and that's what MID servers are for.
But this wasn't just a simple "send print job" scenario. We needed that could handle:
- On-demand printing triggered by ServiceNow state changes
- Load balancing across multiple printers
- Print completion tracking so users knew when cards were ready
- Error handling for failed prints and reprints
- All managed from ServiceNow without requiring manual intervention on the MID servers
- Minimized the weight of physical hardware being transported to Knowledge
The Solution: ServiceNow → MID → Hot Folder → Printer
To solve these challenges, here's the flow we ended up building:
- ServiceNow: Profile record state updates to "Sending to Print Queue"
- Business Rule:
- Custom load balancing picks least busy MID Server
- Creates ecc_queue job to send the print job to the MID Server
- MID Server Execution: JavascriptProbe runs custom script to download image via REST
- File System: Image saved to hot folder on MID server
- DNP Hot Folder Utility: Detects the image file, sends it to the printer, and moves the file to an archive folder
- Print Sync: Scheduled job checks for pending prints that are no longer in the hot folder and updates them to "Pending Card Pickup"
- Completion Notification: User gets in-app notification card is ready
The Hardware Kit: Portable MID Servers
One of the neat things about the printing solution is that it required building out both a software and a hardware solution. Usually, we don't have to concern ourselves with physical devices when developing on ServiceNow. But in this case, we had a lot more to consider about where we would host the MID Server... and how we would get all of our gear to Knowledge.
To that end, here's what we ended up purchasing GMKTec Mini PC's small enough to fit in the palm of your hand to host the MID Server. Each mini PC was connected to a single printer via USB cable. Here's the configuration on those mini PCs:
- 12th Generation Alder Lake N97 processor
- 12GB DDR5 RAM
- Windows 11 Pro
- Connected to network via Lan port
- DNP Dye Sublimation Printer connected via USB
- Docker Desktop Installed
- DNP Hot Folder Print Utility Installed - A 3rd Party Utility provided by the Printer manufacturer
- MID Server running as Linux Docker container
- Folder bind to host shared by the Hot Swap Print Utility and the MID Server
- Portable peripherals that we swapped between the two Mini PCs as needed
- Keyboard
- Mouse
- Monitor
We ran two printers with two MID Servers at Knowledge 25. Fortunately, the Mini PC's, a portable keyboard / mouse combo, portable monitor, and all needed cables fit into my backpack carry on which made transporting to K25 easy enough. Now that you've got a sense of the setup, let's dive deeper into the process.
ServiceNow Side: Orchestrating the Prints
Business Rule: Send to Print Queue
When a card image finishes compositing, a simple business rule kicks off the process:
(function executeRule(current, previous) {
const printingEnabled = (gs.getProperty('x_snc_cctcg_photo.printing_enabled') == 'true');
if (printingEnabled) {
const printer = new C3MidPrinter();
printer.print({ profileId: current.sys_id.toString() });
} else {
current.state = 'Complete';
}
})(current, previous);
The property flag lets us turn printing on/off without code changes - crucial for testing and controlling when printing starts at the conference.
Script Include: C3MidPrinter
The real orchestration happens in the C3MidPrinter script include:
var C3MidPrinter = Class.create();
C3MidPrinter.prototype = {
initialize: function () {},
_selectMidServer: function() {
let selectedMid = {
queueLength: Infinity,
name: '',
};
let queueLengths = {};
// Count pending prints per MID server
let profileAggregate = new GlideAggregate('x_snc_cctcg_photo_profile');
profileAggregate.addQuery('state', 'IN', 'Pending Print,Sending to Print Queue');
profileAggregate.groupBy('mid_printer');
profileAggregate.addAggregate('count');
profileAggregate.query();
while (profileAggregate.next()) {
queueLengths[profileAggregate.mid_printer] = profileAggregate.getAggregate('count');
}
// Find MID server with shortest queue
let mid = new GlideRecord('ecc_agent_application_m2m');
mid.addQuery('agent.status', 'Up');
mid.addQuery('application', '64103b0b47926a10e52659db416d43e5'); // C3 Print application
mid.query();
while (mid.next()) {
let queueLength = queueLengths['mid.server.' + mid.agent.name.toString()] || 0;
if (queueLength < selectedMid.queueLength) {
selectedMid = {
queueLength: queueLength,
name: mid.agent.name.toString(),
};
}
}
return 'mid.server.' + selectedMid.name;
},
print: function ({ profileId, verbose = false }) {
const script = `
var printer = new C3Printer();
printer.print();
`;
var instanceUrl = gs.getProperty('glide.servlet.uri');
var outputFolder = gs.getProperty('x_snc_cctcg_photo.onsite_print_directory', '/home/mid/printqueue/');
var midServer = this._selectMidServer();
// Add trailing slash
outputFolder = outputFolder.endsWith('/') ? outputFolder : outputFolder + '/';
// Create ECC Queue record for JavascriptProbe
var egr = new GlideRecord("ecc_queue");
egr.agent = midServer;
egr.queue = "output";
egr.state = "ready";
egr.topic = "JavascriptProbe";
egr.name = 'C3 Card Print';
egr.payload = `
<?xml version="1.0" encoding="UTF-8"?>
<parameters>
<parameter name="script_type" value="custom_javascript_typ" />
<parameter name="script" value="${script}" />
<parameter name="profileId" value="${profileId}" />
<parameter name="instanceUrl" value="${instanceUrl}"/>
<parameter name="verbose" value="${verbose}"/>
<parameter name="outputFolder" value="${outputFolder}"/>
</parameters>
`;
egr.insert();
// Track which MID server is handling this print
var profileGr = new GlideRecord('x_snc_cctcg_photo_profile');
if (profileGr.get(profileId)) {
profileGr.mid_printer = midServer;
profileGr.update();
}
},
type: 'C3MidPrinter'
};
Custom Load Balancing
The
_selectMidServer()
function implements custom round-robin load. Technically, ServiceNow's MID server implementation does support clustering with load balancing, but it is based on how busy the MID Server itself is. In our setup, the printer and the printer's hot folder queue are the resource constraint, not the MID Server processing. Due to this, we had to write a custom load balancing that looked at the print queue depth by tracking both the print queue via state and the MID Server the queue was on within the CreatorCon C3 Profile. This allowed us to look at how many files were pending print on each MID Server so we could choose the shortest queue.
This keeps both printers busy without overloading either one, even if one stops printing for a moment due to being out of paper or some other technical difficulty. Imagine how bad it could get if a printer stopped running for a few minutes, but we kept sending it jobs. Load balancing based on queue size was a great solution.
We also had to add logic to distinguish between MID Servers for Hackzone and MID Servers for C3, so we made sure the query only targeted MID Servers assigned to the C3 application... and yes, that's a hardcoded sys_id.
MID Server Side: File Operations and Printing
JavascriptProbe: Running Custom Scripts on MID Servers
JavascriptProbes let you execute custom JavaScript on MID servers. The key is setting up the ECC Queue payload correctly:
<parameter name="script_type" value="custom_javascript_typ" />
<parameter name="script" value="var printer = new C3Printer(); printer.print();" />
That
custom_javascript_typ
parameter is critical - it's changed since older MID server versions I was used to, and getting it wrong or leaving it out means your script won't execute.
C3Printer MID Server Script Include
The real work happens in the C3Printer script that runs on the MID server and I gotta give a shout out to John James Andersen on this one. His article was a huge time saver for me on this one:
// Kudos to https://john-james-andersen.com/blog/service-now/javascriptprobe-and-mid-server-script-includes.html
var C3Printer = Class.create();
C3Printer.prototype = {
initialize: function () {
this.File = Packages.java.io.File;
this.FileOutputStream = Packages.java.io.FileOutputStream;
this.HttpClient = Packages.org.apache.commons.httpclient.HttpClient;
this.GetMethod = Packages.org.apache.commons.httpclient.methods.GetMethod;
this.profileId = probe.getParameter('profileId');
this.instanceUrl = probe.getParameter('instanceUrl');
this.verbose = probe.getParameter("verbose");
this.outputFolder = probe.getParameter('outputFolder');
},
print: function () {
try {
this.debug('Download file for profile: ' + this.profileId);
var imageUrl = this.instanceUrl + '/api/x_snc_cctcg_photo/creatorcon_c3_webhooks/image?type=card_for_onsite_print&parentId=' + this.profileId;
var client = new this.HttpClient();
var get = new this.GetMethod(imageUrl);
var status = client.executeMethod(get);
var result;
if (status == "200") {
var filename = this.outputFolder + this.profileId + '.png';
this.debug("Attempted to save the file to filename: " + filename);
var f = new this.File(filename);
var inputStream = get.getResponseBodyAsStream();
var out = new this.FileOutputStream(f);
var buf = Packages.java.lang.reflect.Array.newInstance(Packages.java.lang.Byte.TYPE, 1024);
// Stream file to disk
while ((len = inputStream.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.close();
inputStream.close();
result = {
success: true,
filename: filename,
profileId: this.profileId,
};
} else {
result = {
success: false,
profileId: this.profileId,
message: 'Invalid HTTP Response. Exiting Early.'
};
}
this.debug(result);
return JSON.stringify(result);
} catch (err) {
result = {
success: false,
profileId: this.profileId,
message: err.name,
err: {
message: err.message,
name: err.name,
fileName: err.filename,
lineNumber: err.lineNumber,
stack: err.stack,
}
};
this.debug(result);
return JSON.stringify(result);
}
},
debug: function (m) {
if (this.verbose == "true") {
ms.log(m);
}
},
type: 'C3Printer'
};
Java Package Access for File Operations
The trickiest part was figuring out how to interact with the file system from MID server JavaScript. You have to use Java packages:
this.File = Packages.java.io.File;
this.FileOutputStream = Packages.java.io.FileOutputStream;
This gives you access to Java's file I/O capabilities, letting you write binary data to the MID server's file system.
Print Completion Tracking
Once the file hits the hot folder and gets printed, how do we know it's done? The DNP Hot Folder utility moves printed files to an archive folder, so we can detect completion by checking if the file still exists in the hot folder.
Sync Function on MID Server
This sync function runs on the MID Server (via MID Server Script Include), queries the Profile records that are in the Pending Print state for the given MID Server and checks to see if there is a file in the hot folder. If not, we assume it was printed and moved to the archive folder. So we can update the Profile state to Pending Card Pickup.
syncPrintCompletion: function() {
try {
var updated = [];
var profileGr = new GlideRecord('x_snc_cctcg_photo_profile');
profileGr.addQuery('state', 'Pending Print');
profileGr.addQuery('mid_printer', this.agent);
profileGr.query();
while (profileGr.next()) {
this.debug('Record found: ' + profileGr.sys_id.toString());
var profileId = profileGr.sys_id.toString();
var filename = this.outputFolder + profileId + '.png';
var f = new this.File(filename);
// If file no longer exists, it must have been printed
if (!f.exists()) {
updated.push(profileId);
profileGr.state = 'Pending Card Pickup';
profileGr.update();
}
}
return JSON.stringify({
success: true,
updatedProfiles: updated,
agent: this.agent,
});
} catch (err) {
// Error handling...
}
}
Scheduled Job: Triggering the Sync
We ran the sync via a Scheduled Job which would create the ecc_queue entry to let the MID Server know to sync its print queue with the instance.
const printingEnabled = gs.getProperty('x_snc_cctcg_photo.printing_enabled') == 'true';
if (printingEnabled) {
const printer = new C3MidPrinter();
printer.syncPrintCompletion({ verbose: true });
}
We ran this job every 10 minutes by default but adjusted it when we needed to delay the "Your card is ready notifications" to give our folks cutting cards and putting them in sleeves a chance to get through longer queues. Ideally, we would have scanned a card to mark it complete or had some other mechanism to identify exactly when a card was cut, sleeved, and ready for pickup. But that would have required more time to solution and possibly more hardware so we left that for a later date.
Handling MID Server Responses
Two business rules on the ECC Queue table handle responses from the MID server:
C3 Print Sensor (handles print job responses):
(function executeRule(current, previous) {
printer = new C3MidPrinter();
printer.handleResult(current);
})(current, previous);
C3 Print Completion Sensor (handles sync responses):
(function executeRule(current, previous) {
printer = new C3MidPrinter();
printer.handleResult(current);
})(current, previous);
Both call the same
handleResult
function, which parses the XML response and updates profile records accordingly.
Conference Operations: The Real-World Test
The Good
- Automation worked: Cards automatically flowed from ServiceNow state change to printed output
- Load balancing worked: Both printers stayed busy without either getting overwhelmed
- Reprints were easy: UI action to reset state and replay the sequence
- Error handling was solid: DNP utility ignored corrupt files and kept processing valid ones
- The whole process was tracked from the ServiceNow instance, so very little debugging ever had to be done on the MID Servers
The Bottleneck
The die-cutting and sleeving process became the real constraint. Staff had to:
- Wait for card to print
- Use die cutter to cut card from photo paper
- Slip into clear plastic badge sleeve
- Place on booth counter organized alphabetically
More people jumped in to help and we implemented alphabetical organization, but it stayed wild throughout the conference.
The Race Condition
The sync ran every 10 minutes, so sometimes people got notified their card was ready while it was still being sleeved. Most people showed up at the booth whether they got a notification or not, so this wasn't a major issue.
Key Takeaways for ServiceNow Developers
1. JavascriptProbe is Powerful but Quirky
You can run arbitrary JavaScript on MID servers, but the syntax requirements (especially
script_type
) are finicky and there isn't a lot of documentation or articles about it.
2. Java Package Access Unlocks File Operations
Understanding how to use
Packages.java.io
opens up file system operations that aren't available through standard ServiceNow APIs. This likely applies to some other Java Packages, including the ability to drop custom JAR files on the MID Server. It basically makes the MID Server a cheat code for anything you want to do.
3. Custom Load Balancing Beats Default in some situations
Tracking actual queue depth gave us better printer utilization than ServiceNow's default MID server selection because the printer was the resources constraint, not the MID server itself.
4. Design for Remote Debugging
Return comprehensive data in MID server responses so you can debug from ServiceNow without needing physical access to MID servers. Especially when you are tucking consumer grade MID servers under a cabinet at a conference. May be less important if you've got everything set up to remote into it from the comfort of your desk.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.