Generate & Attach Editable MS Word Document (Prefilled with Incident Details) from Incident Form

Nidhin Viswanat
Tera Contributor

I want to add a "Generate Document” button on the Incident form that creates an editable MS Word file (DOCX preferred) with prefilled Incident details and automatically attaches it to the record. I’m looking for a free, fully OOTB method without paid plugins or external services. 
Key requirements include: 
• Prefill Incident fields (number, short description, priority, assignment group, etc.) 
• Editable Word output 
• Must attach the generated file to the Incident.

1 ACCEPTED SOLUTION

Muhammed Hafees
Tera Expert

@Nidhin Viswanat 

If you're looking to generate Word documents directly from ServiceNow at no additional cost, unfortunately there isn’t a free out‑of‑the‑box feature available for this. ServiceNow does provide a native option through the Word Document Templates (sn_doc_word) plugin, but it is a paid subscription add‑on.
If you prefer a free, custom-built approach, you can absolutely implement this using RTF (Rich Text Format). Since most Word files can interpret RTF content, you can generate the document entirely through scripted logic.
 
Steps for a Custom RTF-Based Word Generator

1. Create an RTF Generator Script Include
Build helper functions to dynamically create RTF content — such as:

  • Headings
  • Paragraphs
  • Field values
  • Tables
    This utility will handle the actual text and formatting generation.

2. Create a Word Generator Utility
This Script Include is responsible for:

  • Constructing the overall template structure
  • Calling the RTF helper methods
  • Packaging the output as a .doc (RTF) file
  • Attaching the final document to the Incident (or any record)
3. Add a UI Action
Create a UI Action (button) on the form—like "Generate Document"—and call your Word Generator utility from there.
 

Notes

  • This method is completely free and works well for standard documents.
  • However, RTF is limited, so complex formatting like advanced tables, images, or rich styling may not look perfect compared to the paid plugin.
  • For many use cases (letters, summaries, formatted business documents), the custom RTF approach is more than sufficient.

1. Script Include: RTFGeneratorUtil 

var RTFGeneratorUtil = Class.create();
RTFGeneratorUtil.prototype = {
    initialize: function() {},
    // ─── Helper: Escape special RTF characters ────────────────────────────────
    rtfEscape: function(input) {
        // Force to JS primitive string — prevents Rhino "Cannot find default value" error
        var str = String(input || '');
        var result = '';
        var len = str.length;
        for (var i = 0; i < len; i++) {
            var c = str.charAt(i);
            var code = str.charCodeAt(i);
            if (c === '\\') {
                result += '\\\\';
            } else if (c === '{') {
                result += '\\{';
            } else if (c === '}') {
                result += '\\}';
            } else if (c === '\r') {
                // skip bare carriage returns
            } else if (c === '\n') {
                result += '\\line ';
            } else if (code > 127) {
                result += '\\u' + code + '?';
            } else {
                result += c;
            }
        }
        return result;
    },
    // ─── Helper: Section heading ──────────────────────────────────────────────
    rtfHeading: function(text) {
        return '{\\pard\\sb280\\sa120\\b\\fs26\\cf1 ' + this.rtfEscape(text) + '\\par}\n';
    },

    // ─── Helper: Label + Value row ────────────────────────────────────────────
    rtfField: function(label, value) {
        return '{\\pard\\sb80\\sa60\\b ' + this.rtfEscape(label) + ':\\b0  ' +
            this.rtfEscape(value || 'N/A') + '\\par}\n';
    },

    // ─── Helper: Body paragraph ───────────────────────────────────────────────
    rtfPara: function(text) {
        return '{\\pard\\sb80\\sa120 ' + this.rtfEscape(text || '(none)') + '\\par}\n';
    },

    // ─── Helper: Horizontal rule ──────────────────────────────────────────────
    rtfRule: function() {
        return '{\\pard\\sb200\\sa200\\brdrb\\brdrs\\brdrw10\\brsp20 \\par}\n';
    },

    // ─── Helper: Horizontal row ──────────────────────────────────────────────

    cellBorders: function() {
        // Common border style for each cell
        return "\\clbrdrt\\brdrs\\brdrw15\\brdrcf1" +
            "\\clbrdrl\\brdrs\\brdrw15\\brdrcf1" +
            "\\clbrdrb\\brdrs\\brdrw15\\brdrcf1" +
            "\\clbrdrr\\brdrs\\brdrw15\\brdrcf1";
    },

    rtfRow(label, value) {
        return "{\\trowd\\trgaph108" +
            this.cellBorders() + "\\cellx3000" +
            this.cellBorders() + "\\cellx9000" +
            "\\intbl " + label + " \\cell" +
            "\\intbl " + value + "\\cell" +
            "\\row}";
    },

    type: 'RTFGeneratorUtil'
};

2. Script Include: WordDocumentUtil

var WordDocumentUtil = Class.create();
WordDocumentUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    generate: function(incNumber) {
        var incGr = this._getIncident(incNumber);
        if (incGr) {
            return this._generateDoc(incGr);
        }
    },
    _getIncident: function(incNumber) {
        var incGr = new GlideRecord('incident');
        incGr.addQuery('number', incNumber);
        incGr.query();

        if (!incGr.next()) {
            gs.error('[WordDoc] Incident not found: ' + incNumber);
            return false;
        }
        return incGr;
    },
    _generateDoc: function(incGr) {
        // CRITICAL: Wrap every field in String() to force JS primitive strings
        // GlideRecord fields return Java objects — Rhino can't iterate them directly
        var incNumber = String(incGr.number);
        var incShortDesc = String(incGr.short_description);
        var incState = String(incGr.state.getDisplayValue());
        var incPriority = String(incGr.priority.getDisplayValue());
        var incDesc = String(incGr.description);
        var title = incNumber ;
        var shortDesc = incShortDesc;
        var subTitle = "Generated : "+now;

        var rtfUtil = new RTFGeneratorUtil();

        // ─── 2. Build RTF Document ────────────────────────────────────────────────
        var now = String(new GlideDateTime().getDisplayValue());

        var rtf =
            '{\\rtf1\\ansi\\ansicpg1252\\deff0\n' +
            // Font table
            '{\\fonttbl\n' +
            '{\\f0\\froman\\fcharset0 Arial;}\n' +
            '{\\f1\\fmodern\\fcharset0 Courier New;}\n' +
            '}\n' +
            // Color table
            '{\\colortbl;\n' +
            '\\red0\\green112\\blue187;\n' + // cf1 — blue
            '\\red80\\green80\\blue80;\n' + // cf2 — grey
            '}\n' +

            '\\widowctrl\\hyphauto\n' +
            '\\margl1800\\margr1800\\margt1440\\margb1440\n' +
            '\\f0\\fs20\n' +

            // Title
            '{\\pard\\sb400\\sa200\\qc\\b\\fs40\\cf1 ' + rtfUtil.rtfEscape(title) + '\\par}\n' +
            '{\\pard\\sb0\\sa120\\qc\\b\\fs26 ' + rtfUtil.rtfEscape(shortDesc) + '\\par}\n' +
            '{\\pard\\sb0\\sa400\\qc\\fs18\\cf2 ' + rtfUtil.rtfEscape(subTitle) + '\\par}\n' +

            rtfUtil.rtfHeading('1. Document Control') +

            "{\\rtf1\\ansi{\\colortbl;\\red0\\green0\\blue0;}" +

            rtfUtil.rtfRow("ServiceNow Incident ID", incNumber) +
            rtfUtil.rtfRow("Short Description", incShortDesc) +
            rtfUtil.rtfRow("Priority", incPriority) +
            "}" +

            // Incident Details
            rtfUtil.rtfHeading('2. Incident Summary') +
			
            rtfUtil.rtfField('Number', incNumber) +
            rtfUtil.rtfField('Short Description', incShortDesc) +
            rtfUtil.rtfField('State', incState) +
            rtfUtil.rtfField('Priority', incPriority) +
            rtfUtil.rtfRule() +

            // Assignment
            rtfUtil.rtfHeading('Assignment') +
            rtfUtil.rtfField('Caller', incCaller) +
            rtfUtil.rtfField('Assigned To', incAssignee) +
            rtfUtil.rtfField('Assignment Group', incAssignGrp) +

            rtfUtil.rtfRule() +

            // Description
            rtfUtil.rtfHeading('Description') +
            rtfUtil.rtfPara(incDesc) +

            // Comments
            (incComments ? (rtfUtil.rtfRule() + rtfUtil.rtfHeading('Additional Comments') + rtfUtil.rtfPara(incComments)) : '') +
            rtfUtil.rtfRule() +

            // Footer
            '{\\pard\\sb200\\sa0\\qc\\fs16\\cf2 Auto-generated by ServiceNow\\par}\n' +
            '{\\pard\\sb40\\sa0\\qc\\fs16\\cf2 ' + rtfUtil.rtfEscape(now) + '\\par}\n' +

            '}';
        this._attachToIncident(incGr, rtf);
    },
    _attachToIncident: function(incGr, document) {

        var gdt = new GlideDateTime();
        var display = gdt.getDisplayValue() + "";
        var formatted = display.replace(/:/g, "-").replace(/\s+/g, "_");
        var fileName = 'Incident_Report_' + incGr.getValue('number') + '_' + formatted + '.doc';
        var contentType = 'application/msword';
        var attachment = new GlideSysAttachment();
        attachment.write(incGr, fileName, contentType, document);
        gs.addInfoMessage("Document " + fileName + " successfully created and attached to " + incGr.getValue('number'));
        return true;
    },
    type: 'WordDocumentUtil'
});

3. Ui Action: Generate Document

var incNumber = current.number;
var generateFile = new WordDocumentUtil().generate(incNumber);
action.setRedirectURL(current);

Output FIle

Screenshot 2026-03-06 115056.png

If this helped, please mark it correct and close the thread for others’ benefit.

View solution in original post

4 REPLIES 4

Ankur Bawiskar
Tera Patron

@Nidhin Viswanat 

I don't think anything is available free for this requirement.

There is a paid app I believe

You will have to rely on external javascript libraries for this if you want to go with free version

💡 If my response helped, please mark it as correct and close the thread 🔒— this helps future readers find the solution faster! 🙏

Regards,
Ankur
Certified Technical Architect  ||  10x ServiceNow MVP  ||  ServiceNow Community Leader

Muhammed Hafees
Tera Expert

@Nidhin Viswanat 

If you're looking to generate Word documents directly from ServiceNow at no additional cost, unfortunately there isn’t a free out‑of‑the‑box feature available for this. ServiceNow does provide a native option through the Word Document Templates (sn_doc_word) plugin, but it is a paid subscription add‑on.
If you prefer a free, custom-built approach, you can absolutely implement this using RTF (Rich Text Format). Since most Word files can interpret RTF content, you can generate the document entirely through scripted logic.
 
Steps for a Custom RTF-Based Word Generator

1. Create an RTF Generator Script Include
Build helper functions to dynamically create RTF content — such as:

  • Headings
  • Paragraphs
  • Field values
  • Tables
    This utility will handle the actual text and formatting generation.

2. Create a Word Generator Utility
This Script Include is responsible for:

  • Constructing the overall template structure
  • Calling the RTF helper methods
  • Packaging the output as a .doc (RTF) file
  • Attaching the final document to the Incident (or any record)
3. Add a UI Action
Create a UI Action (button) on the form—like "Generate Document"—and call your Word Generator utility from there.
 

Notes

  • This method is completely free and works well for standard documents.
  • However, RTF is limited, so complex formatting like advanced tables, images, or rich styling may not look perfect compared to the paid plugin.
  • For many use cases (letters, summaries, formatted business documents), the custom RTF approach is more than sufficient.

1. Script Include: RTFGeneratorUtil 

var RTFGeneratorUtil = Class.create();
RTFGeneratorUtil.prototype = {
    initialize: function() {},
    // ─── Helper: Escape special RTF characters ────────────────────────────────
    rtfEscape: function(input) {
        // Force to JS primitive string — prevents Rhino "Cannot find default value" error
        var str = String(input || '');
        var result = '';
        var len = str.length;
        for (var i = 0; i < len; i++) {
            var c = str.charAt(i);
            var code = str.charCodeAt(i);
            if (c === '\\') {
                result += '\\\\';
            } else if (c === '{') {
                result += '\\{';
            } else if (c === '}') {
                result += '\\}';
            } else if (c === '\r') {
                // skip bare carriage returns
            } else if (c === '\n') {
                result += '\\line ';
            } else if (code > 127) {
                result += '\\u' + code + '?';
            } else {
                result += c;
            }
        }
        return result;
    },
    // ─── Helper: Section heading ──────────────────────────────────────────────
    rtfHeading: function(text) {
        return '{\\pard\\sb280\\sa120\\b\\fs26\\cf1 ' + this.rtfEscape(text) + '\\par}\n';
    },

    // ─── Helper: Label + Value row ────────────────────────────────────────────
    rtfField: function(label, value) {
        return '{\\pard\\sb80\\sa60\\b ' + this.rtfEscape(label) + ':\\b0  ' +
            this.rtfEscape(value || 'N/A') + '\\par}\n';
    },

    // ─── Helper: Body paragraph ───────────────────────────────────────────────
    rtfPara: function(text) {
        return '{\\pard\\sb80\\sa120 ' + this.rtfEscape(text || '(none)') + '\\par}\n';
    },

    // ─── Helper: Horizontal rule ──────────────────────────────────────────────
    rtfRule: function() {
        return '{\\pard\\sb200\\sa200\\brdrb\\brdrs\\brdrw10\\brsp20 \\par}\n';
    },

    // ─── Helper: Horizontal row ──────────────────────────────────────────────

    cellBorders: function() {
        // Common border style for each cell
        return "\\clbrdrt\\brdrs\\brdrw15\\brdrcf1" +
            "\\clbrdrl\\brdrs\\brdrw15\\brdrcf1" +
            "\\clbrdrb\\brdrs\\brdrw15\\brdrcf1" +
            "\\clbrdrr\\brdrs\\brdrw15\\brdrcf1";
    },

    rtfRow(label, value) {
        return "{\\trowd\\trgaph108" +
            this.cellBorders() + "\\cellx3000" +
            this.cellBorders() + "\\cellx9000" +
            "\\intbl " + label + " \\cell" +
            "\\intbl " + value + "\\cell" +
            "\\row}";
    },

    type: 'RTFGeneratorUtil'
};

2. Script Include: WordDocumentUtil

var WordDocumentUtil = Class.create();
WordDocumentUtil.prototype = Object.extendsObject(AbstractAjaxProcessor, {
    generate: function(incNumber) {
        var incGr = this._getIncident(incNumber);
        if (incGr) {
            return this._generateDoc(incGr);
        }
    },
    _getIncident: function(incNumber) {
        var incGr = new GlideRecord('incident');
        incGr.addQuery('number', incNumber);
        incGr.query();

        if (!incGr.next()) {
            gs.error('[WordDoc] Incident not found: ' + incNumber);
            return false;
        }
        return incGr;
    },
    _generateDoc: function(incGr) {
        // CRITICAL: Wrap every field in String() to force JS primitive strings
        // GlideRecord fields return Java objects — Rhino can't iterate them directly
        var incNumber = String(incGr.number);
        var incShortDesc = String(incGr.short_description);
        var incState = String(incGr.state.getDisplayValue());
        var incPriority = String(incGr.priority.getDisplayValue());
        var incDesc = String(incGr.description);
        var title = incNumber ;
        var shortDesc = incShortDesc;
        var subTitle = "Generated : "+now;

        var rtfUtil = new RTFGeneratorUtil();

        // ─── 2. Build RTF Document ────────────────────────────────────────────────
        var now = String(new GlideDateTime().getDisplayValue());

        var rtf =
            '{\\rtf1\\ansi\\ansicpg1252\\deff0\n' +
            // Font table
            '{\\fonttbl\n' +
            '{\\f0\\froman\\fcharset0 Arial;}\n' +
            '{\\f1\\fmodern\\fcharset0 Courier New;}\n' +
            '}\n' +
            // Color table
            '{\\colortbl;\n' +
            '\\red0\\green112\\blue187;\n' + // cf1 — blue
            '\\red80\\green80\\blue80;\n' + // cf2 — grey
            '}\n' +

            '\\widowctrl\\hyphauto\n' +
            '\\margl1800\\margr1800\\margt1440\\margb1440\n' +
            '\\f0\\fs20\n' +

            // Title
            '{\\pard\\sb400\\sa200\\qc\\b\\fs40\\cf1 ' + rtfUtil.rtfEscape(title) + '\\par}\n' +
            '{\\pard\\sb0\\sa120\\qc\\b\\fs26 ' + rtfUtil.rtfEscape(shortDesc) + '\\par}\n' +
            '{\\pard\\sb0\\sa400\\qc\\fs18\\cf2 ' + rtfUtil.rtfEscape(subTitle) + '\\par}\n' +

            rtfUtil.rtfHeading('1. Document Control') +

            "{\\rtf1\\ansi{\\colortbl;\\red0\\green0\\blue0;}" +

            rtfUtil.rtfRow("ServiceNow Incident ID", incNumber) +
            rtfUtil.rtfRow("Short Description", incShortDesc) +
            rtfUtil.rtfRow("Priority", incPriority) +
            "}" +

            // Incident Details
            rtfUtil.rtfHeading('2. Incident Summary') +
			
            rtfUtil.rtfField('Number', incNumber) +
            rtfUtil.rtfField('Short Description', incShortDesc) +
            rtfUtil.rtfField('State', incState) +
            rtfUtil.rtfField('Priority', incPriority) +
            rtfUtil.rtfRule() +

            // Assignment
            rtfUtil.rtfHeading('Assignment') +
            rtfUtil.rtfField('Caller', incCaller) +
            rtfUtil.rtfField('Assigned To', incAssignee) +
            rtfUtil.rtfField('Assignment Group', incAssignGrp) +

            rtfUtil.rtfRule() +

            // Description
            rtfUtil.rtfHeading('Description') +
            rtfUtil.rtfPara(incDesc) +

            // Comments
            (incComments ? (rtfUtil.rtfRule() + rtfUtil.rtfHeading('Additional Comments') + rtfUtil.rtfPara(incComments)) : '') +
            rtfUtil.rtfRule() +

            // Footer
            '{\\pard\\sb200\\sa0\\qc\\fs16\\cf2 Auto-generated by ServiceNow\\par}\n' +
            '{\\pard\\sb40\\sa0\\qc\\fs16\\cf2 ' + rtfUtil.rtfEscape(now) + '\\par}\n' +

            '}';
        this._attachToIncident(incGr, rtf);
    },
    _attachToIncident: function(incGr, document) {

        var gdt = new GlideDateTime();
        var display = gdt.getDisplayValue() + "";
        var formatted = display.replace(/:/g, "-").replace(/\s+/g, "_");
        var fileName = 'Incident_Report_' + incGr.getValue('number') + '_' + formatted + '.doc';
        var contentType = 'application/msword';
        var attachment = new GlideSysAttachment();
        attachment.write(incGr, fileName, contentType, document);
        gs.addInfoMessage("Document " + fileName + " successfully created and attached to " + incGr.getValue('number'));
        return true;
    },
    type: 'WordDocumentUtil'
});

3. Ui Action: Generate Document

var incNumber = current.number;
var generateFile = new WordDocumentUtil().generate(incNumber);
action.setRedirectURL(current);

Output FIle

Screenshot 2026-03-06 115056.png

If this helped, please mark it correct and close the thread for others’ benefit.

Thanks for the explanation. Really helpful!

Thanks for providing the solution — it worked successfully.