Skip to content

Instantly share code, notes, and snippets.

@Jen-Butler
Last active April 2, 2025 13:09
Show Gist options
  • Save Jen-Butler/2e54d82717b231336e926277ddcd8f4e to your computer and use it in GitHub Desktop.
Save Jen-Butler/2e54d82717b231336e926277ddcd8f4e to your computer and use it in GitHub Desktop.
Ticket Review & Closure Process in HaloPSA

Ticket Review & Closure Process

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 Breakdown of Edits We Make to Halo Are Thus:

Pre-Existing Assets

  • 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.


Custom Changes

  • 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 for CFReadyForResolutionControlBox and CFReadyForResolutionNotes fields based on the selection of the single-select field CFReadyForResolution.
    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]

🔍 SQL Breakdown

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.

🧱 Structure of the Query

1. [Warning] Field

📋 What it does:

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”).

2. [HideSave] Field

📋 What it does:

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).

📦 Summary of Table Usage

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

Assign the Lookup Fields

Once the lookup is created and formatted to how you want it, make sure you assign the lookup fields in the custom field mappings: image


🎯 Use Case

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment