Add a button to a configurable workspace form
Your UI Action works perfectly in the classic UI and is simply not there in the workspace. One checkbox and one new script field fix that. A complete escalate button, start to finish.
The problem
You have been writing UI Actions for years. Then your org rolls out a configurable workspace, CSM Workspace, HR Agent Workspace, Service Operations Workspace, take your pick, and an agent asks for the same "Escalate" button they had on the old form. You build the UI Action exactly the way you always have. It shows up beautifully in the classic UI. In the workspace: nothing. No button, no error, no log line, no hint. The first time this happened to me I spent an hour checking ACLs and view rules before I found the answer, which turned out to be a checkbox I had never once needed to look at. This recipe is that hour, compressed.
The quick version
Create a UI Action on the Case table with these settings:
| Field | Value |
|---|---|
| Name | Escalate case |
| Table | Case [sn_customerservice_case] |
| Action name | escalate_major_case |
| Active, Show update, Client | all checked |
| Workspace Form Button | checked (this is the one you were missing) |
| Condition | current.active && gs.hasRole('sn_customerservice_agent') |
In the Workspace Client Script field (it appears once Workspace Form Button is checked):
function onClick(g_form) {
if (g_form.getValue('priority') > 2) {
g_form.addErrorMessage('Raise the case to P1 or P2 before escalating.');
return;
}
// Hands off to the server-side script of this same UI Action
g_form.submit('escalate_major_case');
}
In the regular Script field (the server side):
if (current.canWrite()) {
var grp = new GlideRecord('sys_user_group');
grp.addQuery('name', 'Major Case Response'); // stable name, never a sys_id
grp.setLimit(1);
grp.query();
if (grp.next())
current.assignment_group = grp.getUniqueValue();
current.priority = 1;
current.work_notes = 'Escalated from workspace by ' + gs.getUserDisplayName();
current.update();
gs.addInfoMessage(gs.getMessage('Case escalated to the major case queue.'));
action.setRedirectURL(current);
} else {
gs.addErrorMessage(gs.getMessage('You do not have write access to this case.'));
}
Save, open the case in the workspace, and the button is in the form header actions. Done.
Building it step by step
Step one: the record itself. Go to System UI, UI Actions, and create a new one on your table. Everything here is familiar territory: name, table, order, the Show update checkbox. The two fields that matter for workspace are Workspace Form Button, which is what actually makes the button render in a configurable workspace, and the Workspace Client Script field that appears once you check it. On newer releases you may also see a Format for Configurable Workspace checkbox. If your form has it, check it.
Step two: the condition. The Condition field works exactly like it always has, and it controls visibility in both UIs. Keep it honest: check the record state and the user's role, not just one of them. In my example I gate on current.active and the CSM agent role. The condition is evaluated on the server when the form metadata loads, so anything you can express against current and gs works.
Step three: the workspace client script. This is the piece that replaces your old Onclick script. It must be a function named onClick that receives g_form as a parameter. That parameter is the whole story: in workspace, g_form is handed to you, not floating around as a global you can grab from anywhere. Do your client-side checks here, and when you are ready to run the server-side portion, call g_form.submit('escalate_major_case') with the exact Action name of the UI Action. This is the workspace equivalent of the old gsftSubmit trick.
Step four: the server script. Nothing exotic. current and action are available just like a classic UI Action. Notice I look up the assignment group by name rather than pasting a sys_id, because that record will have a different sys_id in every instance the update set touches, and a name lookup fails loudly instead of silently assigning to nothing. I also check current.canWrite() before touching anything, because the condition field controls visibility, not authorization.
Step five: test it in the workspace. Open the workspace itself, not the classic form. Find a case, and look in the form header actions. If there are several buttons, yours may be under the overflow menu. Click it with a P3 case first to see the client-side error message fire, then bump the priority and run it for real. Check the work notes and the assignment group afterward. If you edit the UI Action while the workspace tab is open, reload the tab: workspace caches form metadata and will cheerfully show you the stale version.
Test as an impersonated agent, not as yourself. Admin passes every role check, so a broken condition looks fine right up until a real agent opens the case. Impersonation is the only honest test of the Condition field.
What is actually happening under the hood
The classic UI renders UI Actions on the server. The button arrives as HTML with your Onclick script inlined, living in the same page as the global g_form and the whole DOM. A configurable workspace is a different animal entirely: it is a Now Experience client application running in the browser, talking to your instance over REST. When it opens a record, it fetches form metadata, and that metadata includes only the UI Actions flagged for workspace whose conditions evaluated true. Your workspace client script ships to the browser as an isolated function, executed with a g_form implementation built for that UI.
That is why the workspace g_form feels familiar but is not identical. The data methods you rely on, getValue, setValue, addErrorMessage, showFieldMsg, setReadOnly, are all there. What is gone is everything that assumed a DOM: getControl does not exist, there is no window or document you should touch, and any old script that reached for jQuery or gel() is dead on arrival. Messages render as workspace notifications rather than the gray bars you are used to. When your client script calls g_form.submit(actionName), the workspace posts the form back and the platform runs your server script in a perfectly ordinary transaction, which is why the server half of this recipe needed no changes at all.
Gotchas
The classic Onclick field is completely ignored in workspace. If your button shows up but does nothing when clicked, you almost certainly have Client checked with an empty Workspace Client Script. And if g_form.submit('...') silently no-ops, the string does not match the UI Action's Action name field exactly.
Two more that bite people. First, the condition is evaluated when the form loads, not continuously. If your button should disappear after it runs (say, once the case is already escalated), that only takes effect after the record saves and the form refreshes, which the g_form.submit round trip does for you. Second, remember that hiding a button is not security. Anyone can invoke actions through other paths, so the server script must re-check canWrite() and any role logic itself. I treat the Condition field as UX and the server script as the actual gate.
Variations worth knowing
Server-only button. If there is nothing to validate client-side, uncheck Client, leave the Workspace Client Script empty, and let the button go straight to the server script. Fewer moving parts, and it behaves identically in both UIs.
Async confirmation. The workspace onClick can return a Promise. Resolve it to let the action proceed, reject to cancel. That is the clean way to do a "are you sure?" flow or a GlideAjax pre-check before committing.
One action, both UIs. You can keep a classic Onclick script and a Workspace Client Script on the same UI Action record. Each UI runs its own flavor, and both funnel into the same server script. This is my default for orgs mid-migration, so behavior never drifts between the two experiences.
Declarative actions. For buttons on lists and related lists in workspace, UI Actions are the old road. The newer model is declarative actions, configured as action assignments that describe what the button does rather than scripting how it renders. Form buttons like this recipe are still comfortably UI Action territory, but if you find yourself fighting to get a button onto a related list in workspace, stop fighting and look at declarative actions instead.