Articles
Building Word documents from CFML with cf-word
Build, populate and convert Word .docx from CFML
Every couple of years I hit the same wall. A client needs a real .docx, not HTML pretending to be one, and CFML has never had a good answer for it. You end up shelling out to a COM object on Windows, or hand-writing OOXML, or gluing Apache POI together with cfscript and hoping the POI version already on the server doesn't fight the one you want to use.
So I built cf-word. It's a free, open-source CFML library for creating, populating, and converting Word documents, and it runs on Adobe ColdFusion 2016+ and Lucee 6+.
What it does
The API is fluent, so most of what you write reads straight down the page:
new cfWord.cfWord()
.addHeading( "Quarterly Report", 1 )
.addParagraph( "Prepared by the analytics team." )
.addTable( [ [ "Region", "Revenue" ], [ "North", "$1.2M" ] ] )
.addHyperlink( "Full documentation", "https://example.com/docs" )
.save( "report.docx" );
That handles the ordinary stuff you always end up needing: headings, paragraphs with bold, italic, color, font, and alignment, tables from arrays or queries or structs, ordered and unordered lists with nesting, images from a file path or a byte array or a base64 string, page setup, and document properties.
A couple of pieces get more use than I expected. One is template population: open a .docx a designer built, fill the {{placeholders}} from a struct with populate(), save. If the template uses real Word bookmarks or content controls instead of plain text tokens, setBookmark() and setContentControl() fill those too, so you're setting the structured fields Word shows in its own UI rather than doing blind find-and-replace.
The other is conversion. fromMarkdown(), fromHTML(), and fromText() turn content into a Word document, and toMarkdown(), toHTML(), and toText() read one back out. Hand it a Markdown file and get a formatted .docx. Hand it a Word doc a user uploaded and pull clean HTML out of it.
The part that took the longest
Apache POI is the obvious engine for this, and it's the reason cf-word can exist at all. It's also the reason it was a pain to build.
Adobe ColdFusion ships its own copy of POI for cfspreadsheet: 3.12 on CF2016, 3.17 on CF2018. Lucee's spreadsheet extension drags one in too. If cf-word just called createObject( "java", ... ), whichever POI the engine loaded first would win, and your document code would be running against a decade-old POI that's missing half the XWPF API.
So cf-word loads its own frozen copy of POI 5.5.1 through an isolated classloader: JavaLoader on ACF, an OSGi bundle on Lucee. Your code gets the exact POI cf-word was tested against, the engine keeps its own, and the two never see each other. There's a check on every run that asks the loader which jar actually served a given class and fails loudly if it isn't cf-word's pinned version. I added that after watching Lucee's bundled commonmark 0.22.0 quietly beat my 0.29.0 until I wrapped the whole dependency set as a proper OSGi bundle.
If you have ever lost an afternoon to jar hell on a shared CF server, you know why this got its own week.
Tested where you'll actually run it
I don't trust "works on my machine" for a library that pokes at Java internals, so cf-word runs its full suite, 283 specs at the moment, on seven engines: ACF 2016, 2018, 2021, 2023, and 2025, plus Lucee 6 and 7, spanning Java 11, 17, and 21. The bookmark and content-control code walks POI's underlying XmlBeans tree directly, which is precisely the kind of thing that breaks quietly between engines, so it gets the most scrutiny. Green on all seven.
If you're on BoxLang
cf-word is for Adobe ColdFusion and Lucee. The method names and feature set are deliberately close to an established, well-designed Word module for BoxLang, because there was no sense reinventing an API that already reads well and that CFML developers will recognize on sight. If you're running BoxLang, that module (bx-word) is the one you want; it needs a BoxLang+ subscription.
Where to get it
It's on GitHub at github.com/JamoCA/cf-word. The version number is 0.1.0, which is honest: the feature set is wide and every part of it is tested across the matrix above, but I'm still knocking off rough edges. Drop the folder into your app with a /cfWord mapping, or install it from the GitHub endpoint with CommandBox.
If you build something with it, or manage to break it, I would genuinely like to hear about it.