This article walks through how to configure SendSafely Halo to work with the ServiceNow Virtual Agent on a Service Portal page. This example prompts the user to upload files for certain workflows (here, an identity-verification flow) and renders the Halo upload button as needed. After the user submits files, the SendSafely Secure Link is written to the ServiceNow interaction record, and the Virtual Agent reads it back to acknowledge the upload in the conversation. No Dropzone Connector is needed for this example.
Note on ServiceNow vs. other chat platforms. Unlike some chat widgets, the out-of-the-box ServiceNow Virtual Agent web client does not expose a client-side API to post a message directly into the conversation, and it renders inside a sealed iframe. This example therefore bridges the bot and the page through a field on the interaction record: the bot sets a flag to request an upload, and the page writes the Secure Link back to the record for the bot (and the live agent) to read.
Prerequisites
- A ServiceNow instance with the Virtual Agent and Agent Chat enabled on a Service Portal.
- Admin access to create a Service Portal widget, add fields to the
interactiontable, and edit the Content Security Policy (CSP). - A SendSafely Dropzone ID and your SendSafely host URL.
Step 1 — Add fields to the Interaction table
The bot and the page communicate through two custom fields on the interaction table. In ServiceNow, go to the Interaction table definition (All → sys_db_object.list → Interaction → Table Columns) and add:
| Field label | Column name | Type | Purpose |
|---|---|---|---|
| SS Open Modal | u_ss_open_modal | True/False | Bot → page flag: request the upload button |
| SS Secure Link | u_ss_secure_link | String (1000) | Secure Link written back after upload |
Step 2 — Allow SendSafely in the Content Security Policy
The Halo modal loads JavaScript from SendSafely, so the Service Portal CSP must allow it. Add your SendSafely hosts (for example files.sendsafely.com and your SendSafely instance host) to the Service Portal CSP entries (All → sys_response_header.list) across the script-src, connect-src, frame-src, and img-src directives. If the button renders but the upload window fails to load, this is usually the cause.
Step 3 — Defining the Trigger in the Virtual Agent
Before configuring Halo, you'll train your Virtual Agent topic to know when to request files and how to acknowledge that files have been uploaded.
Requesting Files
Create (or edit) a topic in Virtual Agent Designer. Give it a trigger phrase — this example uses "I need to verify my identity." The topic introduces the request, runs a Script step that sets the u_ss_open_modal flag, followed by a bot message. Responses that contain a specific phrase will cause the Halo upload button to render on the page. This example uses the phrase "Please use the button below to attach your files."
The Script step that sets the flag when this topic is triggered:
(function execute() {
var gr = new GlideRecord('interaction');
gr.addQuery('opened_for', gs.getUserID());
gr.orderByDesc('sys_created_on');
gr.setLimit(1);
gr.query();
if (gr.next()) {
gr.setValue('u_ss_open_modal', true);
gr.update();
}
})();
Immediately after the Script step, add a Bot Response (text) node with the phrase the page listens for: Please use the button below to attach your files. In the next step, we will add the Halo widget to the Service Portal so that it renders the upload button when the bot sends this phrase.
Acknowledging File Uploads
After the user uploads, the Secure Link is written to the interaction record (see Step 4). Add a confirmation question and a Bot Response (script) node that reads the link back and posts it into the chat:
(function execute() {
var link = '';
var gr = new GlideRecord('interaction');
gr.addQuery('opened_for', gs.getUserID());
gr.orderByDesc('sys_created_on');
gr.setLimit(1);
gr.query();
if (gr.next()) link = gr.getValue('u_ss_secure_link') || '';
var out = new sn_cs.SinglePartOutMsg();
if (link) {
out.setLinkPart('', 'Upload Succeeded', link);
} else {
out.setTextPart("I don't see an upload yet. Please use the button below and try again.");
}
return out;
})();
Once the user is transferred to a live agent (or you create an incident from the chat), the human agent can view the uploaded files using the SendSafely Agent App or by clicking the Secure Link that is stored on the interaction record.
Step 4 — Installing and Initializing the Halo Widget
Create a Service Portal widget (this example names it SendSafely VA Upload) and place it on the Service Portal page that runs the Virtual Agent. The widget has three parts: an HTML template, a Client Script, and a Server Script.
Widget HTML
<div class="sendsafely-hook" style="display:none;"></div>
Widget Client Script
The Client Script loads the Halo modal and defines the upload button. When the bot sends the request phrase, the widget checks the flag on the interaction record and renders the upload button; the user clicks it to open the Halo modal. The onUploadComplete callback writes the Secure Link back to the record and hides the button.
api.controller = function($scope, $timeout) {
var c = this;
var modal = null;
console.log('[SendSafely] controller started');
var DROPZONE_ID = 'put-your-dropzone-id-here';
var SENDSAFELY_URL = 'put-your-sendsafely-host-here';
function loadScript(cb){
if (window.SendSafelyDropzoneModal){ cb(); return; }
var s = document.createElement('script');
s.src = 'https://files.sendsafely.com/js/SendSafelyDropzoneModal.js';
s.onload = cb;
s.onerror = function(){ console.error('[SendSafely] script blocked — check CSP'); };
document.head.appendChild(s);
}
function build(){
return new SendSafelyDropzoneModal({
dropzoneId: DROPZONE_ID,
url: SENDSAFELY_URL,
invokeDropzoneConnectors: false,
submitterEmail: c.data.userEmail || '',
showSubmissionConfirmation: false, // close silently on submit
overlayStyle: { zIndex: '2147483647' },
modalStyle: { zIndex: '2147483647' },
// paperclip icon button (round, pinned by the composer)
uploadButtonText: ' ', // icon only, no label
uploadButtonIcon: "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='20'%20height='20'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='white'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'%3E%3Cpath%20d='M21.44%2011.05l-9.19%209.19a6%206%200%200%201-8.49-8.49l9.19-9.19a4%204%200%200%201%205.66%205.66l-9.2%209.19a2%202%200%200%201-2.83-2.83l8.49-8.48'/%3E%3C/svg%3E",
uploadButtonStyle: {
position: 'fixed',
bottom: '111px', // nudge up/down to align with the composer
right: '51px', // nudge left/right to sit inside the chat panel
width: '40px',
height: '40px',
padding: '0',
maxWidth: '40px',
borderRadius: '9999px',
background: '#2E6B3E',
border: 'none',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
cursor: 'pointer',
boxShadow: 'rgba(0,0,0,0.25) 0px 6px 18px',
zIndex: '2147483647'
},
onUploadComplete: function(url){
console.log('[SendSafely] upload complete:', url);
c.server.get({ action: 'attach_link', link: url }).then(function(r){
console.log('[SendSafely] written to interaction note:', r.data.saved);
});
modal.hideUploadButton();
},
onUploadError: function(t, m){ console.error('[SendSafely] error', t, m); }
});
}
$timeout(function(){
loadScript(function(){
modal = build();
// When the bot sends a message, check the flag; if set, show the paperclip button.
window.addEventListener('message', function(e){
var d = e.data;
if (!d || typeof d !== 'object') return;
if (d.type === 'POST_MESSAGE_FROM_IFRAME') {
setTimeout(function(){
c.server.get({ action: 'check_open' }).then(function(r){
if (r.data.openModal && modal) {
console.log('[SendSafely] open flag true -> showing paperclip button');
modal.showUploadButton();
}
});
}, 2000); // let the bot message render first
}
});
});
}, 1000);
};
Change the button's appearance and position by editing the uploadButtonText, uploadButtonIcon, and uploadButtonStyle properties. In this example the button is pinned just above the Virtual Agent composer.
Widget Server Script
The Server Script provides the user's email to the modal and handles two actions the Client Script calls: check_open (read and clear the request flag) and attach_link (store the Secure Link on the interaction record).
if (input && input.action === 'attach_link' && input.link) {
var gr = new GlideRecord('interaction');
gr.addQuery('opened_for', gs.getUserID());
gr.orderByDesc('sys_created_on');
gr.setLimit(1);
gr.query();
if (gr.next()) {
gr.work_notes = 'Secure file uploaded via SendSafely: ' + input.link; // agent-visible
gr.setValue('u_ss_secure_link', input.link); // clean field for the bot
gr.update();
data.saved = true;
} else {
data.saved = false;
}
}
// Bot → widget: should we open the modal?
if (input && input.action === 'check_open') {
data.openModal = false;
var gr = new GlideRecord('interaction');
gr.addQuery('opened_for', gs.getUserID());
gr.orderByDesc('sys_created_on');
gr.setLimit(1);
gr.query();
if (gr.next() && gr.getValue('u_ss_open_modal') == '1') {
data.openModal = true;
gr.setValue('u_ss_open_modal', false); // clear so it opens only once
gr.update();
}
}
Step 5 — Using the Halo modal
Now that the topic and widget are in place, you're ready to test. The topic prompts the user to verify their identity and asks them to upload their document.
When the user confirms, the bot responds with "Please use the button below to attach your files," which renders the Paper Clip Icon button just above the chat composer.
When the user presses the button, the SendSafely Halo modal appears and allows them to attach one or more files.
Once the files are attached and the user presses submit, the modal closes and the Secure Link is written to the interaction record. After the confirmation step, the bot posts the Secure Link into the chat.
The Secure Link is also visible to human agents on the interaction record (work notes or description and the u_ss_secure_link field), so it carries through to a live-agent handoff or an incident created from the chat.
Optional — Agent-initiated upload requests
A live agent can also request an upload mid-conversation. Create a UI Action named Request Secure Upload on the interaction table (workspace form button enabled) that sets the same flag:
(function() {
var gr = new GlideRecord('interaction');
if (gr.get(current.getUniqueValue())) {
gr.setValue('u_ss_open_modal', true);
gr.setWorkflow(false);
gr.update();
}
gs.addInfoMessage('Secure upload prompt sent to the user.');
})();
When the agent clicks the button and then sends a chat message, the page renders the Halo upload button on the user's side, exactly as in the bot flow.
Notes and tips
- Keep
invokeDropzoneConnectors: falseso the Secure Link is handled by theonUploadCompletecallback rather than a Dropzone Connector. showSubmissionConfirmation: falsecloses the modal automatically after a successful upload.- The upload button is triggered by the bot's request phrase. If you change that phrase in the topic, no change is needed in the widget (the widget reads the flag, not the phrase) — but the request phrase must be sent as a bot message so the page checks the flag.
- The
u_ss_secure_linkvalue must be read after the upload completes; the topic's wait and confirmation step ensure the link is present before the bot reads it.
Comments
0 comments
Article is closed for comments.