Hello Community,
I’m currently got a new use case where we have an existing Excel form that includes multiple text fields and checkboxes. The goal is to digitally fill out this form and then generate a PDF from it — all within a ThingWorx-based environment.
Here’s the idea I’m considering:
Convert the Excel structure into an HTML representation (including form fields and styling).
Open this HTML form dynamically in the browser via ThingWorx.
Allow users to fill in the fields (text, checkboxes, etc.).
Finally, generate and save the completed form as a PDF.
My question is:
Is this workflow achievable directly in ThingWorx?
Has anyone implemented something similar using ThingWorx + HTML + PDF generation?
What are the best practices I can consider for this?
Note: We have around 80 plus excel files of customer which they normally fill out and save as pdf. I cannot make it static checklist in vuforia studio because there can be in the future 100 plus excels which needs to be automate. So basically its just digitalization of checklist which needs to be filled and saved as pdf thats it.
Any input, examples, or suggestions would be greatly appreciated!
Thanks in advance,
In my this post of mine , i was successfully able to open HTML in browser from thingworx
https://community.ptc.com/t5/ThingWorx-Developers/Generate-a-Pre-Filled-HTML-Template-in-Browser-for-PDF-Export-Is/m-p/1018231#M69880
Solved! Go to Solution.
Hello @MA8731174,
I would look in a different direction if I were you. I'd rather hack something like this:
In other words, I would delegate to MS Office for all that Excel filling and PDF generation. This all assumes that all of that is exposed via REST APIs, and it is easy to use it, which is a very big assumption. It probably isn't easy at all.
/ Constantine
Hello @MA8731174,
I would look in a different direction if I were you. I'd rather hack something like this:
In other words, I would delegate to MS Office for all that Excel filling and PDF generation. This all assumes that all of that is exposed via REST APIs, and it is easy to use it, which is a very big assumption. It probably isn't easy at all.
/ Constantine
Out of curiosity I did a quick check, and it seems like all those REST APIs are indeed available:
/ Constantine
Thank @Constantine for your feedback but for this process would you please also explain where should i call these apis? you meant with MS Azure ? I am a bit confused about the process to implement as i have never used MS Office APIs before. would you elaborate a bit how should i start implementing this approach?
The flow would be like this:
Here I make a bunch of important assumptions:
On a side note, regardless of the approach you take, I think it's a pretty complex feature. You are likely looking at several weeks of implementation work.
/ Constantine
Thank you so much @Constantine for your detailed recommendation – your suggested approach is working well so far, and I’m making good progress with the implementation.
I’m not quite finished yet, but I’ll be sure to update you once everything is working or if I run into any further questions.
Really appreciate your support and guidance!
As you can see above when i click on template button which triggers navigation and i can see the excel file in mashup after doing copy file and generating link all in the background.. now i cannot find any event in navigate with which i can close this navigation with button from mashup. What i have found so far is that i can close the popup which is chrome itself popup and then this popupCancelled event get called but i would like may be something on mashup may be to close the popup of excel. what do you think about it?
I don't think there's much you can do about it. First of all, Office's CORS policy won't allow you to embed this form in your mashup. Second, you can't open / close windows from mashup Expressions, as they explicitly block "window" object.
The only solution I can think of would be to craft a simple UI extension, which would allow doing something like this:
It's literally a ten lines of code extension. If you've never done ThingWorx UI extensions, you can either go through Help, or use something like this as a quickstart: https://github.com/vilia-fr/twx-infotable-to-json-ext
/ Constantine
Just wanted to give a quick update regarding the process:
I click the button in the mashup to open the Excel file (via link), make some changes, and then close the window. On closing, I trigger the PDF generation service to create, save in repository and display the PDF on the mashup.
However, I noticed an issue: approximately once in every 15 tries, the changes made in the Excel file do not appear in the generated PDF. To resolve this, I have to reopen the Excel file—then the changes are visible quickly in excel which i did before, and when I close it again, the PDF reflects them correctly.
This inconsistency is a bit of a blocker for me at the moment. I also tried adding pause(2000) before calling the PDF service, but the issue still occurs occasionally. I feel like its not about pdf service , its about excel saving the content....
I am testing it with a very basic Excel file where I only change the value in one cell before closing, and this is the behaviour. I hope i would find a way here...
Try something -- when you reproduce this behavior, before opening this spreadsheet in the browser again, try to open it in another browser, or in the Private mode. See if you can still see your changes. Maybe they stay in your browser's local storage before being synced to the backend. In this case that's something you'd need to google for, e.g. "how to flush Office 365 changes to OneDrive" -- there might be some frontend trick to do this.
I did some testing using two different browsers — I opened the mashup in Chrome and had the same file open in Edge. In both cases, changes made in one tab were instantly visible in the other, so it was clearly not a browser cache issue.
Afterward, I tried using the lastModifiedDateTime parameter provided by the Graph API with the file metadata. I compared that value with Date.now() and added a wait loop that only proceeds once the lastModifiedDateTime has changed.
},
"file": {
"mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"hashes": {
"quickXorHash": "rf5vEi9WxaBtqa8W7kKsKpej/Io="
}
},
"fileSystemInfo": {
"createdDateTime": "2025-08-05T08:53:09Z",
"lastModifiedDateTime": "2025-08-07T09:18:56Z"
},
"shared": {
"scope": "users"
}
With this approach, everything seems to be working well now. I'm still testing, but so far it's looking stable and reliable.
My javascript service in thingworx as below:- which waits untill the lastmodifieddatetime changes for file in onedrive
let lastModifiedDateTime = Things["SmartPDFAssist.APICalls.Controller"].getFilelastModifiedDateTime(); // e.g. "2025-08-07T08:16:12Z"
let lastModified = new Date(lastModifiedDateTime.lastModifiedDateTime);
let now = new Date();
let result;
let diffInMs = now.getTime() - lastModified.getTime();
let diffInSeconds = diffInMs / 1000;
if (Math.abs(diffInSeconds) < 1) {
executeService();
logger.info("Exact same datetime (to the second).");
logger.info(now.getTime() - lastModified.getTime());
result="Exact same datetime (to the second).";
}
else if (diffInSeconds > 5) {
for (let i=0;i<14; i++){
pause(1000);
lastModifiedDateTime = Things["SmartPDFAssist.APICalls.Controller"].getFilelastModifiedDateTime();
logger.info("wait for the " + i + " time");
if (now.getTime() <= new Date(lastModifiedDateTime.lastModifiedDateTime).getTime()) {
break;
}
}
executeService();
logger.info("Last modified time is older than 5 seconds.");
result ="Last modified time is older than 5 seconds.";
}
else if (diffInSeconds > 0 && diffInSeconds <= 5) {
//pause(6000);
executeService();
logger.info("Last modified time is less than 5 seconds behind current time.");
result ="Last modified time is less than 5 seconds behind current time.";
}
else if(diffInSeconds < 0) {
executeService();
logger.info(" Last modified time is in the future compared to now.");
result = " Last modified time is in the future compared to now.";
}
else {
result = "no condition worked";
}
function executeService() {
let pdfFile = Things["SmartPDFAssist.APICalls.Controller"].saveOneDriveFileAsPDF();
Things["SmartPDFAssist.FileRepository"].SaveBinary({path:"/test/files/testfile.pdf",content:pdfFile});
}
@MA8731174 First of all, congratulations! I am positively surprised it worked at the end, well done!
Second, keep in mind that this approach is very sensitive to your local clock being correct. Make sure you have an NTP service ("Windows Time Service") running on your server, so that your clock is synchronized!
Third, this logic can be streamlined:
let elapsed = function() {
return now.getTime() - new Date(Things["SmartPDFAssist.APICalls.Controller"].getFilelastModifiedDateTime().lastModifiedDateTime).getTime();
}
while ((ms = elapsed()) > 1000) {
// The last modification is older than 1s, which means OneDrive hasn't saved the file yet
if (ms > 15000) {
throw new Error("FATAL: OneDrive didn't save the file within 15 seconds - please retry");
}
pause(500); // Let's wait a bit longer... The delay should be < 1000ms to avoid infinite loops.
}
/ Constantine
Thank you so much for continuously supporting! Glad to be part of such a great helping community . So as you said the time approach can be tricky i have found another thing which is
eTag: The etag represents the overall state of an item (metadata and content)
cTag: The ctag specifically tracks changes to the item's content.
eTag and cTag we also get back as we get lastmodifieddatetime so i just try to use them instead ....so on close excel file i get the present eTag and cTag which is in graphAPI saved and when that service completed i call the next service which returns me eTag and cTag again which is after closing the file and i compare them with my on close values and if they are not changed take delay and check again untill they are change.... i have delay upto 45 or 50 seconds.... one thing which i have found very weired that even though eTag and cTag are changed the pdf changes can still take a second to be fully saved in the cloud of DRIVE ... even though i already had 2 seconds delay before we get our pdf still it came once wrong...so i just put another 2 seconds delay before calling the pdf service as you can see below....
I am doubting a bit on my approach now. i dont know either it is right or can cause problems later.... i have tested now with this code now pdf is being generated right with latest changes.... i have tested it around 50 times now.. what do you think about it?
// ========= Pre-close snapshot (baseline) =========
// Grab the current tags *right when the user closes the Excel window*.
// We will wait until OneDrive shows a version newer than this.
var baseMeta = Things["SmartPDFAssist.Data.Controller"].getSourceFileCTag({ folderName: "", fileName: "" });
// Expecting baseMeta to include eTag / cTag (and optionally hashes.quickXorHash)
var baseETag = baseMeta.eTag;
var baseCTag = baseMeta.cTag;
var baseHash = (baseMeta.hashes && baseMeta.hashes.quickXorHash) ? baseMeta.hashes.quickXorHash : undefined;
logger.info("Baseline -> eTag: " + baseETag + " | cTag: " + baseCTag + " | hash: " + (baseHash || "-"));
// ========= Backoff schedule (balanced correctness vs speed) =========
// Total wait ~60–70s max. Adjust if you see longer commits in practice.
var waits = [1000, 1000, 2000, 3000, 5000, 8000, 13000, 15000];
var ready = false;
var meta, curHash;
// ========= Poll until tags/hash change from baseline =========
for (var i = 0; i < waits.length; i++) {
pause(waits[i]); // wait before checking again to let OneDrive commit/version-bump
meta = Things["SmartPDFAssist.Data.Controller"].getSourceFileCTag({ folderName: "", fileName: "" });
curHash = (meta.hashes && meta.hashes.quickXorHash) ? meta.hashes.quickXorHash : undefined;
var changed =
(meta.eTag !== baseETag) ||
(meta.cTag !== baseCTag) ||
(!!baseHash && !!curHash && curHash !== baseHash); // use content hash if both sides available
logger.info(
"Wait " + (i + 1) + "/" + waits.length + " (" + waits[i] + " ms) -> " +
"eTag: " + meta.eTag + " (base " + baseETag + "), " +
"cTag: " + meta.cTag + " (base " + baseCTag + "), " +
"hash: " + (curHash || "-") + " (base " + (baseHash || "-") + "), " +
"changed=" + changed
);
if (changed) {
// small settle time even after version bump to avoid race with export
pause(2000);
ready = true;
break;
}
}
// ========= Generate PDF =========
if (ready) {
executeService();
logger.info("PDF generated after OneDrive version bump was detected.");
} else {
logger.warn("Timed out waiting for OneDrive version bump (~>60s). Generating PDF anyway.");
// IF no change has been done that means the cTag eTag would not change then we show the old pdf already.
executeService();
}
// ========= Helper =========
function executeService() {
pause(2000);
var pdfFile = Things["SmartPDFAssist.APICalls.Controller"].saveOneDriveFileAsPDF();
Things["SmartPDFAssist.FileRepository"].SaveBinary({
path: "/test/files/testfile.pdf",
content: pdfFile
});
}
If we take a step back... do you need a PDF immediately? Maybe it's enough to store a "promise" in your database and generate PDF on the fly at the moment when you need to read it? Lazy PDF generation of sorts. If you don't clean up those spreadsheets in OneDrive, you should be able to generate PDF anytime later, even in one or two years... Taking it even further, you don't even need to store those PDFs in ThingWorx, you can always generate them anew when you need them.
Another wild idea -- ThingWorx mashups are just JSONs, which you can generate yourself, if you want to. You might be able to create some script (e.g. an Excel macro) that would convert your spreadsheet into a mashup for you. This assumes you know how to program Office macros well.
Just an idea, if you have Analytics installed, you might want to look into the Analytics Manager which comes with an Excel Connector. This could get you the Excel populated and computed, but for the last step - is rendering an Excel as a PDF really something you want your IIOT platform to perform?
Hello @MA8731174,
It looks like you have some responses from some community members. If any of these replies helped you solve your question please mark the appropriate reply as the Accepted Solution.
Or if you were able to figure out the issue, please help to document it and mark the reply as your accepted solution. This will surely help other community members who may have a similar question.
Thanks,
Vivek N.
Community Moderation Team.
