Understanding Attachments in ServiceNow

Attachments in ServiceNow are not as straight-forward as email attachments, and it's not always obvious how to do what you want with them.

Recently, I needed to copy some attachments programmatically and otherwise fiddle around with attachments. After finding nothing in the ServiceNow product documentation, nothing on the wiki, and very little through the usual search channels, I figured it was time to write an article about how to programmatically deal with attachments in ServiceNow. 

Note: This article discusses some little-documented APIs. As always, be careful to note anywhere you use such APIs, in case they are not supported in a future update. I recommend using try {} catch() {} blocks around any undocumented function, with a very clear log message in the catch.

Index

Attachments: How do they work?

You may be aware that you can store more than just text within ServiceNow, in the form of attachments; you can store binary data as well, including pictures, audio, and even executable files. But, what happens when you upload a binary file as an attachment, to ServiceNow?

There are two tables which do the work of dealing with attachments -- Attachments (sys_attachment), and Attachment Documents (sys_attachment_doc). When you upload an attachment file to ServiceNow, a record is created in the Attachments table with some metadata, including the filename, content type, and the size of the attached file.

The actual binary data of the file is split into ~4KB (3736 byte) chunks, which are then saved into the data field of the Attachment Documents table. This table has a few other interesting fields, such as length (which defines the number of bytes stored in this chunk), and position (which defines what order this chunk of bytes fits in). The Attachment Documents table also contains a reference field (sys_attachment), which points to the parent record in the Attachments table. 

This means that each attachment file will have one (or probably more) entries in the sys_attachment_doc table, which corresponds to the binary data of the original file that was attached. The Position field determines the order for re-combining the data into the original file. 

ServiceNow Attachment Documents table binary data

Now that we understand more about the nature of attachments, we can easily come up with some cool ways to manipulate and work with them! 

Getting Text From an Attachment

Whether you're reading a text file that came over from an import, or parsing the XML payload of a Shazzam discovery probe that ended up being too many characters for the payload field on the form, it is not uncommon to need to parse text (especially XML) from an attachment.

Luckily, the way forward is not too terribly difficult. Below, I've included some code that will do just that for you, followed by an explanation of how it works! 

var tableName = 'u_test_case';
var sysIDOfRecord = '78e47fe94f31d200bf004a318110c7d4';
//Declare a new instance of GlideSysAttachment. 
var gsa = new GlideSysAttachment();
//Get the raw bytes in the file
var bytesInFile = gsa.getBytes(tableName, sysIDOfRecord); 
//Convert that jive into a string using Java/Rhino. 
var dataAsString = Packages.java.lang.String(bytesInFile);
//Re-convert to a string in Javascript, cause we don't trust Rhino.
dataAsString = String(dataAsString);

gs.print(dataAsString);

On lines 1 and 2, I'm just declaring the table and record that I'm working with. In your case, these values will be different, and probably assigned through some loop or something, depending on how you're using this code. 

On line 4, I declare an instance of the GlideSysAttachment class, as the variable gsa. While this class is used in two scripts in the ServiceNow documentation, there is no actual documentation on it; so as I mentioned above, I recommend wrapping this in a try/catch block. 

On line 6, I use an undocumented method of the GlideSysAttachment class: getBytes. This returns the raw bytes of the file -- we're talking about binary data here. This sets the stage for what we'll do next on line 8, where we convert that data into a string using Packages.java.lang.String().

Why such a strange package call? The byte data exists within the underpinning framework that ServiceNow uses, which is called Rhino. Rhino runs Java (not JavaScript), and doesn't like to share super neat data types such as bytes with the freshman kids, like JavaScript. So if we want to convert these bytes into a string, we have to do it the Rhino way first, by using Packages.java.lang.String(). If we don't, we might get something lame and unhelpful, like "[B@89e840". That's no good.

Then, on line 10, we re-cast the variable to a String in JavaScript - primarily because I don't trust Rhino's typecasting - and finally, we print out the string we got from our attachment. If it was a text-based file, we should get something like this, displaying the text that was in the document: 

"That works pretty swell" - I hear you say - "but hey wait a minute -- I only told the GlideSysAttachment class the table my record was on, and the sys_id of the record. What if my record had multiple attachments? How would it know which one to get the bytes for, and all that jive?"

Good question -- it doesn't. .getBytes() always only grab the most recent attachment. If this is not your text file, you might end up outputting a string that looks like this: 

This... is also not helpful. 

So, how do we determine which attachment gets parsed? Well, I was able to find the GlideSysAttachment.get() method, and after a short while spent throwing various forms of data at it, I found that there is indeed a simple way to get the text out of a specific attachment on a record! Here it is: 

var tableName = 'u_test_case';
var sysIDOfRecord = '78e47fe94f31d200bf004a318110c7d4';
var fileNameSansExtension = 'test'; //Full file name: test.txt

var gr = new GlideRecord(tableName);
gr.get(sysIDOfRecord);

var gsa = new GlideSysAttachment();
var textVal = gsa.get(gr, fileNameSansExtension);

gs.print(textVal);

The new and important bit is on line 6 above, in which we call .get(), and pass in a GlideRecord object containing the record with which the attachment is associated, and a string containing the file name (without the filetype extension)

If we use this method, once again, we get the output that we are hoping for - the text contents of our document: 

Calculating MD5 Hash/Checksums

Note: In Geneva, the calculateMD5CheckSum method of the GlideChecksum class used below is not accessible unless you submit a ticket to ServiceNow, requesting that it be whitelisted.  Unfortunately, this is the only way to calculate an MD5 checksum using native functionality in ServiceNow Geneva. It should work in earlier versions, though.

There are a multitude of reasons that one might need to calculate MD5 checksums for attachments in ServiceNow. Here are some examples: 

  1. You've uploaded a file using SOAP/REST, and want to verify that the bytes came across correctly, so you have the system send back the MD5 checksum.
  2. You want to check whether the file on the server matches the file you're intent on uploading. If so, don't upload, to save bandwidth.
  3. You want to verify the integrity of an external database that includes mirrored attachments from ServiceNow, but don't want to send across the entire file just to validate it. 

Unfortunately, it sure doesn't seem to be easy to generate an MD5 checksum from a file attachment in ServiceNow -- which makes sense, if you think about it. The files are split into a myriad of 4KB pieces across your database, stored as strings of base64 encoded binary data. In order to generate an MD5 checksum, you'll have to re-construct that file first -- so that's just what we'll do. 

var gr = new GlideRecord('sys_attachment'); //the table where attachment metadata is stored
gr.get('0003ea666f015600623008efae3ee4f7'); //sys_id of a record in the sys_attachment table
var md5HashSum = calculateMD5Hash(gr);

function calculateMD5Hash(attachmentGR) {
    var attInptStream = GlideSysAttachmentInputStream(attachmentGR.sys_id + '');
    var chksum = (new GlideChecksum()).calculateMD5CheckSum(attInptStream);
    gs.print(chksum);
}

On lines 1-2, we get a GlideRecord referring to a specific record on the sys_attachment table. On line 3, we call our function. 

On line 6, we instantiate an undocumented class: GlideSysAttachmentInputStream, and pass the sys_id of the attachment record to its' constructor. 

On line 7, we actually calculate the MD5 checksum using a method of the GlideChecksum class: calculateMD5CheckSum(), and we pass in the attachment input stream that we created on line 6.

That's all there is to it. Don't worry if you don't understand entirely what these functions are doing. They're hidden deep inside ServiceNow, and you're unfortunately not really meant to understand precisely how they work. 

Copying/Moving an Attachment

Copying all of the attachments associated with a given record is fairly straightforward. You simply call the copy method of the nearly-undocumented GlideSysAttachment class, and pass in four strings: 

  1. The table you want to copy the attachment from (incident, change_request, etc.).
  2. The sys_ID of the record you want to copy the attachment from
  3. The table that you want to copy the attachment to.
  4. The sys_ID of the record you want to copy the attachment to

This might look something like this: 

var donorTable = 'incident';
var donorID = '2b6644b15f1021001c9b2572f2b47763';
var recipientTable = 'incident';
var recipientID = '78e47fe94f31d200bf004a318110c7d4';

GlideSysAttachment.copy(donorTable, donorID, recipientTable, recipientID);

Again however, you might notice that we haven't actually told GlideSysAttachment which attachment we want it to copy over. In this case, that just means that it'll copy over all of the attachments associated with the record we've chosen. 

Unfortunately, there doesn't appear to be a built-in way to copy one specific attachment associated with a record, when you have multiple. That's okay though -- now that we understand how attachments work, we can write our own! 

copySpecificAttachment(donorTable, donorID, recipientTable, recipientID, fileName);

function copySpecificAttachment(donorTable, donorID, recipientTable, recipientID, fileName) {
    var donorAttSysID;
    var newAttRecord;
    var linkToNewRecord;
    var attDataRecord;
    var newDocRecord;
    var attRecord = new GlideRecord('sys_attachment');
    attRecord.addQuery('table_name', donorTable);
    attRecord.addQuery('table_sys_id', donorID);
    attRecord.addQuery('file_name', fileName);
    attRecord.query();
    while (attRecord.next()) {
        donorAttSysID = attRecord.getValue('sys_id');
        newAttRecord = copyRecord(attRecord);
        newAttRecord.setValue('table_name', recipientTable);
        newAttRecord.setValue('table_sys_id', recipientID);
        newAttRecord.update();
        linkToNewRecord = gs.getProperty('glide.servlet.uri') + newAttRecord.getLink();
        attDataRecord = new GlideRecord('sys_attachment_doc');
        attDataRecord.addQuery('sys_attachment', donorAttSysID);
        attDataRecord.query();
        while (attDataRecord.next()) {
            newDocRecord = copyRecord(attDataRecord);
            newDocRecord.setValue('sys_attachment', newAttRecord.getValue('sys_id'));
            newDocRecord.update();
        }
    }
    //gs.print(linkToNewRecord);
}
function copyRecord(record) {
    var recordElement;
    var recordElementName;
    var recordTable = record.getTableName();
    var recordFields = record.getFields();
    var newRecord = new GlideRecord(recordTable);
    newRecord.initialize();
    for (var i = 0; i < recordFields.size(); i++) {
        recordElement = recordFields.get(i);
        if(recordElement.getName() != 'sys_id' && recordElement.getName() != 'number')
        {
            recordElementName = recordElement.getName();
            newRecord.setValue(recordElementName, record.getValue(recordElementName));
        }
    }
    var newSysId = newRecord.insert();
    return newRecord;
}
Pro Tip: See how I didn't document my code pretty much at all in the example above? Don't do that. I removed any comments from my code because I'm going to explain it line-by-line in just a sec, but you should always do your future self a favor, and document your code thoroughly

Wow, that's some pretty dense code there. Let's step back, and look at the high-level breakdown of what it's doing: 

  1. Lines 4-8 are just setting up our environment. It is technically best practice to declare all or nearly all of your variables at the top of your function, but many people don't like doing it that way. It doesn't hurt anything if you don't do it that way, so do whatever you're comfortable with. 
  2. Lines 9-13: Get a GlideRecord with the actual sys_attachment record we're looking to copy.
  3. Line 16: Copy that sys_attachment record, using our custom copyRecord function. This super nifty function grabs all the fields on a given record, and makes a duplicate, copying over the values of every field (except sys_id, and number). 
    1. Remember, the sys_attachment record essentially just contains metadata about the attachment. The actual bytes contained within the file, are stored in the sys_attachment_doc table! 
  4. Lines 17-19: Since our copyRecord function returns the GlideRecord object for the newly created record, we can now modify a couple of fields. In this case, we're modifying the table_name and table_sys_id fields. This way, we've got an exact copy of the record we passed into that function, except for those two changes we've made. These changes associate the new sys_attachment record to our "recipient" record. 
  5. Lines 21-31: We create a new GlideRecord for the sys_attachment_doc table (which is where the data for the attachments is actually stored, so we'll need to copy those entries too). We use a query to make sure we get all of the sys_attachment_doc records related to our specific sys_attachment record, and then we pass each one into our handy-dandy copyRecord function. Finally, we modify and then .update() the returned GlideRecord containing the new sys_attach_doc records. 

Attachment Properties

The following are a list of attachment-related properties that you can access and modify in ServiceNow by typing sys_properties.list into the application navigator filter bar (and pressing Enter, if you're on Geneva+/UI16+). You can either modify the value of a property in that list, or (if you don't see a given property in the list), create it and set the value specified below. 

Attachment Properties
Property Values Description
glide.ui.attachment_drag_and_drop true/false Enable/disable drag-and-drop to attach a file (in supported HTML5 compatible browsers)
com.glide.attachment.max_size Integer (MB) The maximum size (in megabytes) for individual file attachments. Leaving this field blanks allows attachments up to 1GB.
glide.attachment.role Comma-separated list Enter a comma-separated list of the roles that can attach files. Enter names, not sys_ids. Entering "Public", or leaving this blank means that anyone can attach files.
glide.attachment.extensions Comma-separated list Enter a comma-separated list of attachment file extensions (not including the 'dot') which should be allowed. To allow all, leave this property value blank. Note that unless this is blank, any attachments not listed here will be blocked.
glide.ui.disable_attachment_view & glide.ui.attachment_popup true/false To hide the "[View]" link next to an attachment, set the disable_attachment_view property to true and the attachment_popup property to false. Users can still view attachments by clicking the attachment file name.
glide.ui.attachment.
force_download_all_mime_types
true/false This property, when true, makes it so that all attachment file types will be downloaded rather than viewed in the browser, when clicked. This is especially helpful for filetypes like text or HTML files, where the browser may attempt to render them rather than download them.

Like the idea of being able to copy specific attachments, or specific records, but don't feel like rewriting all that code and repurposing it for your instance? No problem, you lazy jerk - we've got you covered! Just download the update set XML below, and deploy it into your instance - here's how: 

  1. Download and extract this tool, and save it to your computer.
  2. Elevate privileges to the security_admin role.
  3. Navigate to System Update Sets -> Retrieved Update Sets.
  4. Click the Import Update Set from XML link.
  5. Click Choose File and select the XML file you downloaded, then click Upload
  6. Commit the update set. 
    1. Open the update set you just uploaded
    2. Click Commit Update Set

And that's it, you're done! 
Thanks for reading, don't forget to subscribe, and happy developing! 


SN Pro Tips is owned and written by The SN Guys, ServiceNow consultants. If your organization could benefit from our services, we humbly welcome you to schedule some time to chat and share your goals with us, so we can help your business Grow With Purpose


Love learning? Check out some of our other articles below!