Update for Yokohama:
On Yokohama, the original business rule in this article which aborted the delete of an attachment caused the UI to not show the attachment as deleted until the screen was refreshed. The updated BR below copies the attachment to the same record, prepending the file name with “DELETED-“. It shows up in the activity log as being uploaded at the time that it was delete.
(function executeRule(current, previous /*null when async*/ ) {
if (current.table_name.getValue().slice(0, 5) == 'ZZ_YY') {
//if it is for an attachment field, remove the attachment's sys_id from the fields
removeAttachmentFromField();
//copy the attachment to a new attachment before deleting it
tableName = current.table_name.getValue().replace('ZZ_YY', '');
copyAttachment(tableName, current.getValue('table_sys_id'));
} else {
//copy the attachment to a new attachment before deleting it
copyAttachment(current.table_name.getValue(), current.table_sys_id.getValue());
}
function copyAttachment(targetTableName, targetSysId) {
var targetGR = new GlideRecord(targetTableName);
targetGR.get(targetSysId);
var fileName = 'DELETED-' + current.getValue('file_name');
var contentType = current.getValue('content_type');
var sourceAttachmentSysId = current.getValue('sys_id');
// Attach sys_attachment record content stream to test_table record
var gsa = new GlideSysAttachment();
var newAttachmentSysId = gsa.writeContentStream(
targetGR,
fileName,
contentType,
gsa.getContentStream(current.getUniqueValue()));
//prepend the table name with ZZ_YY
var newAttachmentGR = new GlideRecord('sys_attachment');
newAttachmentGR.get(newAttachmentSysId);
newAttachmentGR.setValue('table_name', 'ZZ_YY' + targetGR.getTableName());
newAttachmentGR.update();
}
function removeAttachmentFromField() {
//Normally when an attachment is deleted from the attachment field, the actual attachment record is deleted but the field retains the sys_id of the delete attachment (stupid, yes, but HI confirmed that this is how it works.). This function removes the sys_id from the field.
//get the table name
var tableName = current.table_name.getDisplayValue().replace('ZZ_YY', '');
//get the GR for the host record
var hostRecord = new GlideRecord(tableName);
hostRecord.get(current.table_sys_id);
//find all the attachment fields and look for one with the sys_id of the attachment
var dictGR = new GlideRecord('sys_dictionary');
dictGR.addQuery('name', tableName);
dictGR.addQuery('internal_type', 'file_attachment');
dictGR.query();
while (dictGR.next()) {
if (hostRecord.getValue(dictGR.element.getValue()) == current.getUniqueValue()) {
hostRecord.setValue(dictGR.element.getValue(), ''); //remove the sys_id
}
}
hostRecord.setWorkflow(false);
hostRecord.update();
}
})(current, previous);
Without fail, users that are new to ServiceNow always ask about how to recover deleted attachments. Especially on attachment fields where the Delete button is so darn close to the Update button, a stray mouseclick can mean the end of the attachment. Unfortunately, out-of-box, attachments that are deleted by the user are truly deleted. “So you’re telling me that I can’t delete or edit Work Notes but I can delete attachments without any trace?” Yes. Kind of annoying.
To make matters worse, what regular users wouldn’t notice but admins might is that on attachment fields, the system deletes the attachment but leaves the sys_id of the deleted attachment in the field. That means if you have any scripts that check to see if there is an attachment present in the field, you can’t just check for a value in the field. You also have to check that the sys_id exists in the sys_attachment table.
The below Business Rule is an answer to this common pitfall. This is handy for HRSD where attachments usually have more business signficance than in ITSM but it could be used anywhere in the platform.
The purpose of this Business Rule is to prevent the deletion of attachments on specific tables. If the attachment being deleted is in an attachment field, the BR will delete the sys_id from the attachment_field (which should be out of the box functionality anyway, in my opinion) but leave the attachment record in the sys_attachment table. If the attachment being deleted is a regular attachment, the BR will change the table name in the sys_attachment record to ZZ_YY, effectively hiding it from the attachments section at the top of the form. In both cases, the attachment will be available in the activity formatter, in the entry from when the attachment was originally uploaded.
Here’s how the BR is configured, in this case it is only triggered for attachments in the Human Resources application

Here is the script in the Advanced tab
(function executeRule(current, previous /*null when async*/ ) {
if (current.table_name.getValue().slice(0, 5) == 'ZZ_YY') {
//if it is for an attachment field
removeAttachmentFromField();
} else {
//otherwise, if it is just a regular attachment, prefix the table name to ZZ_YY, this will hide it in the attachments section
current.setValue('table_name', 'ZZ_YY' + current.table_name.getValue());
current.update();
current.setAbortAction(true);
}
//abort the delete action
current.setAbortAction(true);
function removeAttachmentFromField() {
//Normally when an attachment is deleted from the attachment field, the actual attachment record is deleted but the field retains the sys_id of the delete attachment. This function removes the sys_id from the field.
//get the table name
var tableName = current.table_name.getDisplayValue().replace('ZZ_YY', '');
//get the GR for the host record
var hostRecord = new GlideRecord(tableName);
hostRecord.get(current.table_sys_id);
//find all the attachment fields and look for one with the sys_id of the attachment
var dictGR = new GlideRecord('sys_dictionary');
dictGR.addQuery('name', tableName);
dictGR.addQuery('internal_type', 'file_attachment');
dictGR.query();
while (dictGR.next()) {
if (hostRecord.getValue(dictGR.element.getValue()) == current.getUniqueValue()) {
hostRecord.setValue(dictGR.element.getValue(), ''); //remove the sys_id
}
}
hostRecord.setWorkflow(false);
hostRecord.update();
}
}
)(current, previous);