A customer submits a support form. An opportunity opens in a dedicated Support pipeline and becomes the ticket. Every stage has its own automation, so dragging a card to a stage fires exactly the right actions and the right customer email. Drop your brand, your categories, and your team in - the engine doesn't change.
A customer submits the Submit a Support Ticket form. That creates or matches a contact, and the Intake router checks whether they already have an open ticket. If they do, the message is added to it. If not, the New sequence generates a ticket number, opens an opportunity in the Support pipeline, sends the received email, and tags the ticket by category. The opportunity is the ticket - its stage is the status, and all ticket data lives on opportunity fields so every ticket is self-contained. Each stage runs its own automation the moment the card lands there.
The form writes to contact fields as a capture layer. The New sequence copies them onto opportunity fields, where the real ticket record lives - so a customer who opens a second ticket never overwrites the first.
Create a pipeline named Support Tickets with five stages, in order. The stage is the ticket status, and each stage has its own automation that fires the instant a card lands there.
| Stage | Meaning | Its automation (on entry) |
|---|---|---|
| New | Just arrived (set on form submit) | Ticket ID + open opportunity + copy fields + Email 1 + category router |
| Urgent | Manual priority flag | Tag priority-urgent + email alert to the team (SMS optional). No customer email. |
| In Progress | Team actively working it | End the Awaiting Reply timer + clear the urgent flag. Optional "we're on it" email. |
| Awaiting Reply | Waiting on the customer | Email 2 (needs info), remind at 3 days, auto-close at 5 |
| Resolved | Closed | End the Awaiting Reply timer + open a 7-day reopen window + stamp Date Resolved + Email 3 + optional CSAT survey |
The form writes to contact fields. The New sequence copies them onto opportunity fields, where the real ticket record lives - so a customer with multiple tickets never has one ticket overwrite another.
| Field | Type | Merge key |
|---|---|---|
| Request Category | Dropdown | {{contact.request_category}} |
| Support Type | Dropdown | {{contact.support_type}} |
| Describe Your Issue | Multi-line text | {{contact.describe_your_issue}} |
| Attachment URL | Text | {{contact.attachment_url}} |
| Support Ticket Number (scratchpad) | Text | {{contact.support_ticket_number}} |
| Field | Type | Merge key | Filled by |
|---|---|---|---|
| Ticket Number | Text | {{opportunity.ticket_number}} | New seq |
| Ticket Category | Text | {{opportunity.ticket_category}} | copy |
| Ticket Support Type | Text | {{opportunity.ticket_support_type}} | copy |
| Ticket Issue | Multi-line text | {{opportunity.ticket_issue}} | copy |
| Ticket Attachment URL | Text | {{opportunity.ticket_attachment_url}} | copy |
| Ticket Additional Info Needed | Multi-line text | {{opportunity.ticket_additional_info_needed}} | Agent |
| Ticket Resolution | Multi-line text | {{opportunity.ticket_resolution}} | Agent |
| Ticket Date Resolved | Date | {{opportunity.ticket_date_resolved}} | Resolved seq (auto) |
In the New sequence, build the ticket number from GHL's date and time tags: TKT-{{right_now.date}}-{{right_now.time}}:{{right_now.second}} → TKT-06/17/2026-14:30:52. Unique to the second; no counter needed, and no premium formatter step.
A Custom Value counter plus a Math Operation step works, but two tickets arriving together can read the same counter value before either writes it back. The date ID has no such race, so it stays the default.
A GHL form named Submit a Support Ticket. GHL's form create/clone API is unreliable, so the form is built in the form builder - but every custom field it maps to was created in Step 2, so it's just dragging those fields in.
Swap these for your own products, departments, or service lines - the router reads whatever you put here.
A second axis so the team can triage by intent, independent of category.
Subjects and bodies use merge tags so each ticket personalizes itself. Set the From name/email to [Company] Support <support@yourcompany.com> and Reply-To to the same inbox, so customer replies thread back and trigger the reply handler. The first four are below; Email 5 lives with the Intake router in Step 8.
Hi {{contact.first_name}},
Thanks for reaching out. We've received your request and opened Support Ticket #{{opportunity.ticket_number}}.
Our team is on it and will get back to you within {{custom_values.support_sla}}.
Need to add something? Just reply to this email and it'll attach straight to your ticket.
- The {{custom_values.company_name}} Support Team
Hi {{contact.first_name}},
We're working on Ticket #{{opportunity.ticket_number}} and need one more thing from you to move it forward:
Just reply to this email with that and we'll pick it right back up.
Hi {{contact.first_name}},
Good news - Ticket #{{opportunity.ticket_number}} has been resolved.
If anything still isn't right, just reply to this email and we'll reopen it. Thanks for your patience.
Hi {{contact.first_name}},
We're holding Ticket #{{opportunity.ticket_number}} open and still waiting on one thing from you:
Reply whenever you have a moment and we'll finish up. If we don't hear back, we'll close the ticket in a couple of days - you can always reply to reopen it.
|
|||||||||||||||||||||||||||||
<!-- STATUS: swap the pill word per email - Received / Awaiting your reply / Resolved.
PROGRESS: move the navy bar + bold label to the current step (Received / In progress / Resolved). -->
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all;font-size:1px;line-height:1px;color:#F4F4ED;opacity:0;">Ticket {{opportunity.ticket_number}} is open. Reply to this email to add details.</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#F4F4ED;">
<tr>
<td align="center" style="text-align:center;padding:32px 12px 40px;">
<!--[if mso]><table role="presentation" width="600" align="center" cellpadding="0" cellspacing="0" border="0"><tr><td><![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;margin:0 auto;text-align:left;background:#FFFFFF;border:1px solid #E6E6DE;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
<!-- Header: wordmark + mailbox name -->
<tr>
<td style="padding:22px 28px;border-bottom:1px solid #EDEDE6;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-size:16px;line-height:20px;font-weight:700;letter-spacing:-0.2px;color:#0B1E3F;">{{custom_values.company_name}}</td>
<td align="right" style="text-align:right;font-size:13px;line-height:20px;color:#5A6479;">Support</td>
</tr>
</table>
</td>
</tr>
<!-- Ticket identity -->
<tr>
<td style="padding:28px 28px 0;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="font-size:12px;line-height:24px;font-weight:600;letter-spacing:0.8px;text-transform:uppercase;color:#5A6479;">Support ticket</td>
<td align="right" style="text-align:right;line-height:24px;">
<span style="display:inline-block;padding:3px 11px;border-radius:999px;background:#FFF1E6;font-size:12px;line-height:18px;font-weight:600;color:#A84300;white-space:nowrap;"><span style="display:inline-block;width:6px;height:6px;border-radius:3px;background:#FF7A1A;vertical-align:middle;margin:0 6px 2px 0;"></span>Received</span>
</td>
</tr>
<tr>
<td colspan="2" style="padding-top:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace;font-size:22px;line-height:30px;font-weight:600;letter-spacing:-0.3px;color:#0B1E3F;">{{opportunity.ticket_number}}</td>
</tr>
</table>
<!-- Meta rows -->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:20px;font-size:14px;line-height:20px;">
<tr>
<td style="padding:10px 0;border-top:1px solid #EDEDE6;color:#5A6479;">Category</td>
<td align="right" style="text-align:right;padding:10px 0;border-top:1px solid #EDEDE6;color:#0B1E3F;font-weight:600;">{{opportunity.ticket_category}}</td>
</tr>
<tr>
<td style="padding:10px 0;border-top:1px solid #EDEDE6;color:#5A6479;">Request type</td>
<td align="right" style="text-align:right;padding:10px 0;border-top:1px solid #EDEDE6;color:#0B1E3F;font-weight:600;">{{opportunity.ticket_support_type}}</td>
</tr>
<tr>
<td style="padding:10px 0;border-top:1px solid #EDEDE6;border-bottom:1px solid #EDEDE6;color:#5A6479;">Expected reply</td>
<td align="right" style="text-align:right;padding:10px 0;border-top:1px solid #EDEDE6;border-bottom:1px solid #EDEDE6;color:#0B1E3F;font-weight:600;">Within {{custom_values.support_sla}}</td>
</tr>
</table>
<!-- Progress: Received > In progress > Resolved -->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top:22px;">
<tr>
<td width="32%" style="height:3px;line-height:3px;font-size:0;background:#0B1E3F;border-radius:2px;"> </td>
<td width="2%" style="font-size:0;line-height:3px;"> </td>
<td width="32%" style="height:3px;line-height:3px;font-size:0;background:#E4E4DC;border-radius:2px;"> </td>
<td width="2%" style="font-size:0;line-height:3px;"> </td>
<td width="32%" style="height:3px;line-height:3px;font-size:0;background:#E4E4DC;border-radius:2px;"> </td>
</tr>
<tr>
<td style="padding-top:8px;white-space:nowrap;font-size:12px;line-height:16px;font-weight:700;color:#0B1E3F;">Received</td>
<td></td>
<td style="padding-top:8px;white-space:nowrap;font-size:12px;line-height:16px;color:#5A6479;">In progress</td>
<td></td>
<td style="padding-top:8px;white-space:nowrap;font-size:12px;line-height:16px;color:#5A6479;">Resolved</td>
</tr>
</table>
</td>
</tr>
<!-- Body copy slot -->
<tr>
<td style="padding:28px 28px 4px;font-size:16px;line-height:26px;color:#243A5E;">
<!-- EMAIL BODY COPY GOES HERE -->
<p style="margin:0 0 16px;">Hi {{contact.first_name}},</p>
<p style="margin:0;">Thanks for reaching out. Your request is in, and the {{custom_values.company_name}} team will pick it up within {{custom_values.support_sla}}. Here is what you sent us, for reference.</p>
</td>
</tr>
<!-- CALLOUT (optional): relabel per email - "What we need from you" + {{opportunity.ticket_additional_info_needed}}, or "Resolution" + {{opportunity.ticket_resolution}}. Delete this row if unused. -->
<tr>
<td style="padding:8px 28px 24px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#F4F4ED;border-radius:10px;">
<tr>
<td style="padding:16px 18px;">
<div style="font-size:12px;line-height:16px;font-weight:600;letter-spacing:0.8px;text-transform:uppercase;color:#5A6479;">Your request</div>
<div style="padding-top:6px;font-size:15px;line-height:23px;color:#0B1E3F;">{{opportunity.ticket_issue}}</div>
</td>
</tr>
</table>
</td>
</tr>
<!-- Reply hint: a reply threads onto this ticket -->
<tr>
<td style="padding:18px 28px 22px;border-top:1px solid #EDEDE6;font-size:14px;line-height:21px;color:#5A6479;">
<strong style="color:#0B1E3F;font-weight:600;">Reply to this email</strong> to add details or a screenshot. Your reply attaches to ticket <span style="white-space:nowrap;">{{opportunity.ticket_number}}</span>, so there is no need to open a new one.
</td>
</tr>
</table>
<!-- Footer -->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
<tr>
<td align="center" style="text-align:center;padding:20px 28px 0;font-size:12px;line-height:18px;color:#5A6479;">{{custom_values.company_name}} Support - you are receiving this because you opened a support ticket.</td>
</tr>
</table>
<!--[if mso]></td></tr></table><![endif]-->
</td>
</tr>
</table>
The form captures Request Category, and the New sequence's router branch reads it: every category gets its own tag so the board stays sortable. The tag is what's category-specific; the owners (your support team) are constant. The customer emails stay shared across all categories.
| Category | Tag added | Routing branch |
|---|---|---|
| Billing & payments | support-billing | tag → notify support team |
| Technical issue | support-technical | tag → notify support team (+ optional dev escalation) |
| Account & access | support-access | tag → notify support team |
| Onboarding & setup | support-onboarding | tag → notify support team |
| Product question | support-product | tag → notify support team |
| Other (catch-all) | support-general | tag → notify support team (the else so nothing is unrouted) |
Every stage is its own workflow, triggered by Pipeline Stage Changed filtered to this pipeline + stage. The New sequence has no trigger of its own - the Intake router (Step 8) owns the form and starts it with Add to Workflow. Drag a card to a stage and that stage's sequence fires - nothing overlaps. One cross-stage reply handler keeps the stages honest, and a small Reopen window timer tells it how long a resolved ticket can still be reopened. Build each as a Draft, then publish after a test ticket runs clean. Click a workflow to expand its steps.
The only stage a ticket enters automatically. Build order matters: release → number → opportunity → copy fields → email → router. Everything below is manual-drag triggered. GHL note: turn on Allow Re-Entry - a returning customer enters this workflow once per ticket.
No customer email - Urgent is internal priority only. Agent SOP: drag any New / In Progress ticket here to escalate. No task either: the Support Tickets board is the work queue, and a task per stage entry stacks duplicates every time a ticket comes back.
This is also where the Reply handler and the Intake router land a ticket the customer just answered, or a reopened one - and both already notify the team. GHL note: turn on Allow Re-Entry in this workflow's settings - a ticket can enter In Progress more than once (after a reopen or a reply), and with re-entry off the second entry won't fire.
GHL note: a contact still inside this workflow's wait is skipped when a ticket enters Awaiting Reply again - Allow Re-Entry only applies once the earlier run has ended. Ask a customer for more info twice in five days and the second Email 2 silently never sends. That is why every way out of this stage (In Progress, Resolved, and a brand-new ticket) starts with Remove From Workflow: Awaiting Reply sequence. The If/Else checks stay as a second guard.
This stage also receives auto-closed tickets from the Awaiting Reply timeout (resolution already filled with the "no response" text).
Why the lookup: Customer Replied knows the contact, not the ticket. One open ticket per customer (Step 8) is what makes "latest in Support Tickets" always the right card. The 7-day window stops a reply to a sales email months later from reopening an old ticket - past that, the customer submits the form and gets a fresh one. GHL note: don't gate this on a date condition. An If/Else "in the last 7 days" on an opportunity date field saves fine but never evaluates true; the tag is checked reliably.
Why its own workflow: the wait can't sit inside the Resolved sequence. A contact still waiting there would be skipped the next time one of their tickets is resolved - no Email 3. Turn on Allow Re-Entry.
Every form submission goes through the Intake router first. It looks the customer up in the Support pipeline itself: no open ticket, a new one opens; an open ticket already there, the new message is added to it - no second card, nothing thrown away, and the customer hears back straight away.
Why a router in front of the New sequence? The New sequence stays one straight path that only ever opens tickets. The router owns the form trigger and decides; Add to Workflow hands off in a single step, so there's no hand-off tag to get stuck. Test once: submit twice from the same email and confirm Email 5 shows the first ticket's number.
| Latest Support ticket | Router does |
|---|---|
| None | Opens a new ticket (New sequence) |
| New Urgent In Progress | Adds the message to it, pings the owner, sends Email 5 |
| Awaiting Reply | Same, and moves it to In Progress - the customer just gave us what we asked for |
| Resolved, or marked Lost / Won / Abandoned | Opens a new ticket |
Hi {{contact.first_name}},
Thanks for the extra detail. You already have an open ticket with us - #{{opportunity.ticket_number}} - so we've added this message to it instead of opening a new one.
The same team is on it, with everything you've sent in one place. Want to add more? Just reply to this email.
- The {{custom_values.company_name}} Support Team
Every ticket is its own opportunity, so a returning customer ends up with several opportunities in the same Support pipeline. GHL blocks that by default, and it fails quietly:
| Case | Handled by |
|---|---|
| Same customer opens 2+ tickets | Ticket data on the opportunity = each ticket independent (no overwrite) |
| Customer submits again while a ticket is open | Intake router adds the message to the open ticket, pings the owner, and sends Email 5 - no second card, nothing lost |
| Ticket marked Lost, Won, or deleted | The router reads the pipeline, so the next submission opens a fresh ticket - nothing to unlock |
| Customer replies to a sales email months after a ticket closed | Reply handler only reopens tickets resolved in the last 7 days |
| Double-click: two submissions in the same second | Both can land before the first card exists and open two tickets. Rare - mark the duplicate Lost |
| Ticket is about a specific category | The New sequence's router tags the category + notifies the team |
| Customer replies during Awaiting Reply | Reply handler moves it back to In Progress + notifies |
| Customer replies after Resolved | Reply handler reopens to In Progress + notifies |
| Customer never replies (stuck) | Awaiting Reply reminds at 3 days, auto-closes at 5 |
| Agent asks for more info a second time within 5 days | In Progress and Resolved end the old Awaiting Reply run first, so the second Email 2 always sends |
| No extra info needed (straight to fix) | Agent fills Resolution, drags to Resolved; Awaiting Reply is optional |
| File / screenshot attached | Form file field stored on contact; link surfaced in the team notification |
| Ticket # uniqueness | Padded date + time to the second; no two moments produce the same number |
The stage is the status. The opportunity is the ticket. Nobody writes a reply by hand.
Swap the brand, the categories, and the team - the engine stays exactly the same.