Let’s talk a little bit about the trials and tribulations that MSPs go through surrounding the ticket closure process. A tale as old as time – tasks or appointments go uncompleted as the ticket is closed. Yes, you do have it set to warn if there are open tasks before closing the ticket – but nothing is really preventing the technician from forcing it through, only for your dispatch to review it later and send it back to the technician to actually complete, or you just ‘let it be’ and wonder if the tasks had ever really been completed at all, worse yet maybe searching through the ticket and wasting time trying to figure it out.
This certainly isn’t a new problem, but there are ways to mitigate the hassle of sending the ticket back and forth and hopefully capture the information you want and need in the process.
What we’re discussing today involves making some customizations to your Halo instance that end up forcing your technicians to handle tickets a certain way. In this case, think about the areas in Halo or the different actions that allow your technicians to change the status of a ticket to a Closed or Completed status, or resolve it with an action.
Before you restrict the ability to access the statuses you don’t want, you should make sure the actions and workflows have been created and/or modified accordingly to allow technicians to follow the new normal of how you want them to process tickets for your scenario. Unless, of course, you thrive in a chaotic environment (I know the types).
-
The Quick Status Dropdown:
Remove the Closed and Completed statuses from the quick status change dropdown on tickets. This forces technicians to use the actions and statuses you’ve defined. -
The Current Action Edits:
Define a custom list of statuses for each existing action that uses the Status field, that a technician will have access to during the stages of your preferred workflow.
-
The Resolve Action:
Create a new action called Resolve (or your preferred flavor of ‘all set’). -
The Custom Fields:
Create at least three custom fields.- One single selection field for the lookup trigger
- Two rich text fields; one for displaying our message, and one for controlling the HTML.
- Make sure you add them to the new action and set dynamic visibility on the rich text fields based on the single selection field.
-
The Incident Management (or your preferred) Workflow:
Update the workflow by removing the Resolve Ticket action from the 1st, 2nd, and 3rd stages.
Create another stage specifically for resolution and tie that to the custom Resolve action.
Now, only the Resolve action is used, which must be completed to advance to the next step—where dispatch is notified, and the ticket can then be reopened or truly resolved. -
The Canned Text Values:
SQL does not preserve capitalization that you enter directly, but it does preserve it when rendering canned text.
Add your preferred messaging to some canned text values that we can import in the lookup. -
The Lookup:
Create a SQL lookup to populate the (what I named) custom fields forCFReadyForResolutionControlBox
andCFReadyForResolutionNotes
fields based on the selection of the single-select fieldCFReadyForResolution
.
We will go over the SQL below:
SELECT
CASE
WHEN EXISTS (
SELECT 1 FROM faulttodo WHERE ftfaultid = $TicketId AND ftdone = 'False'
)
OR EXISTS (
SELECT 1 FROM appointment WHERE APFaultid = $TicketId AND APCompleteStatus = -1 AND APisTask = 'False'
)
OR EXISTS (
SELECT 1 FROM appointment WHERE APFaultid = $TicketId AND APCompleteStatus = -1 AND APisTask = 'True'
)
THEN
'<style>
.w {font-size:18px;color:rgb(184,49,47);font-weight:700;background-color:#f8d7da;padding:15px;border:2px solid #d9534f;border-radius:5px;text-align:left;}
.w ul {padding-left:40px;list-style-type:disc;list-style-position: outside;margin:0}
.w li {margin-bottom:10px;font-weight:700;color:rgb(184,49,47)}
.u {text-transform:uppercase}
.s {display:block;padding-left:0px;font-size:14px;font-weight:400;margin-top:2px;color:rgb(184,49,47)}
</style>
<div class="w">'
+ CAST((SELECT CThtml FROM cannedtext WHERE CTid = 5) AS NVARCHAR(MAX))
+ '<ul>'
+ ISNULL((
SELECT
'<div><span class="u">To-Do</span><ul>'
+ STRING_AGG('<li><span class="s">' + CAST(FT.FTNote AS NVARCHAR(MAX)) + '</span></li>', '')
+ '</ul></div>'
FROM faulttodo FT
WHERE FT.ftfaultid = $TicketId AND FT.ftdone = 'False'
), '')
+ ISNULL((
SELECT
'<div><span class="u">Appointment</span><ul>'
+ STRING_AGG('<li><span class="s">' + CAST(AP.APsubject AS NVARCHAR(MAX)) + '</span></li>', '')
+ '</ul></div>'
FROM appointment AP
WHERE AP.APFaultid = $TicketId AND APCompleteStatus = -1 AND APisTask = 'False'
), '')
+ ISNULL((
SELECT
'<div><span class="u">Task</span><ul>'
+ STRING_AGG('<li><span class="s">' + CAST(AP.APsubject AS NVARCHAR(MAX)) + '</span></li>', '')
+ '</ul></div>'
FROM appointment AP
WHERE AP.APFaultid = $TicketId AND APCompleteStatus = -1 AND APisTask = 'True'
), '')
+ '</ul>'
+ CAST((SELECT CThtml FROM cannedtext WHERE CTid = 4) AS NVARCHAR(MAX))
+ '</div>'
ELSE
CAST((SELECT CAST(CThtml AS NVARCHAR(MAX)) FROM cannedtext WHERE CTid = 3) AS NVARCHAR(MAX))
END AS [Warning],
CASE
WHEN EXISTS (
SELECT 1 FROM faulttodo WHERE ftfaultid=$TicketId AND ftdone='False'
)
OR EXISTS (
SELECT 1 FROM appointment WHERE APFaultid=$TicketId AND APCompleteStatus=-1 AND APisTask='False'
)
OR EXISTS (
SELECT 1 FROM appointment WHERE APFaultid=$TicketId AND APCompleteStatus=-1 AND APisTask='True'
)
THEN
'<style>
.details-form-controls input[type="submit"] { display: none; }
label[for="input-field-for-customfield_234"],
label[for="input-field-for-customfield_234"] + div { display: none; }
</style>'
ELSE
'<style>
.details-form-controls input[type="submit"] { display: inline-block; }
label[for="input-field-for-customfield_234"],
label[for="input-field-for-customfield_234"] + div { display: none; }
</style>'
END AS [HideSave]
This SQL lookup is used to dynamically control the display of warnings and UI elements during the resolution phase of a ticket in Halo. It evaluates whether any To-Dos, Appointments, or Tasks are still incomplete before allowing the ticket to move forward.
Renders a styled warning message if there are any uncompleted items related to the ticket.
CASE
WHEN EXISTS (
SELECT 1 FROM faulttodo WHERE ftfaultid = $TicketId AND ftdone = 'False'
)
- Checks if To-Dos exist that are not marked done.
OR EXISTS (
SELECT 1 FROM appointment WHERE APFaultid = $TicketId AND APCompleteStatus = -1 AND APisTask = 'False'
)
- Checks for incomplete Appointments (non-task).
OR EXISTS (
SELECT 1 FROM appointment WHERE APFaultid = $TicketId AND APCompleteStatus = -1 AND APisTask = 'True'
)
- Checks for incomplete Tasks (marked as tasks).
If any of the above return true:
'<style>...</style><div class="w">...</div>'
- Injects the canned text HTML we created in previous steps.
- Uses
cannedtext
to pull predefined messaging (CTid = 5
for intro,CTid = 4
for outro). - Lists:
- Incomplete To-Dos
- Incomplete Appointments
- Incomplete Tasks
All wrapped in structured HTML for visibility and impact. This is required to preserve capitalization unless you want to do even more complicated things and formatting with HTML and CSS directly, which is also an option.
If nothing is pending:
CAST((SELECT CAST(CThtml AS NVARCHAR(MAX)) FROM cannedtext WHERE CTid = 3) AS NVARCHAR(MAX))
- Loads a positive, ready-to-resolve message (e.g. “All items complete”).
Controls visibility of the Save/Submit button based on task/appointment completion.
If any tasks or appointments are not complete:
'<style>
.details-form-controls input[type="submit"] { display: none; }
label[for="input-field-for-customfield_234"],
label[for="input-field-for-customfield_234"] + div { display: none; }
</style>'
- Hides the Save/Submit button.
- Also hides a specific field label (
customfield_234
) and its container. - 234 is the id of the custom field you want to manipulate.
If all are complete:
'<style>
.details-form-controls input[type="submit"] { display: inline-block; }
label[for="input-field-for-customfield_234"],
label[for="input-field-for-customfield_234"] + div { display: none; }
</style>'
- Shows the Save/Submit button.
- Keeps the label field hidden (assumed to be system-related).
Table | Purpose |
---|---|
faulttodo |
Checks if To-Do items are completed |
appointment |
Differentiates between appointments and tasks using APisTask |
cannedtext |
Stores reusable HTML/text for warnings/info |
Once the lookup is created and formatted to how you want it, make sure you assign the lookup fields in the custom field mappings:
This logic ensures:
- A ticket cannot be resolved without finishing associated items.
- The technician is shown exactly what’s incomplete.
- The submit button is disabled until resolution criteria are met.
This promotes accuracy, accountability, and reduced rework across the team.