Best viewed on a desktop - the flow diagrams are wide. On mobile, swipe a diagram to explore it.
BrewedOps
Reusable support-desk blueprint
An automated support desk, built inside GoHighLevel
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.
0Pipeline
0Stages
0Emails
0Workflows
0Manual replies
Built inside GoHighLevel
The flow
How it works
A customer submits the Submit a Support Ticket form. That creates or matches a contact and fires the New sequence, which 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.
Gap closed
Per-ticket isolation. If ticket data lived on the contact, a customer opening a 2nd ticket would overwrite the 1st ticket's number, issue, and resolution (GHL matches contacts by email), and the older ticket's emails would reference the newer ticket's data. Putting ticket data on the opportunity makes every ticket independent. The contact fields are only a transient capture layer the New sequence copies onto the opportunity.
Step 1 · Pipeline & stages
One pipeline, five stages
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 + alert (email + SMS) to the team. No customer email.
In Progress
Team actively working it
Assign owner task + clear stale flags. 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
Stamp Date Resolved + Email 3 + optional CSAT survey
Step 2 · Custom fields
Two field sets: capture & record
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.
C Contact fields (the form fills these)
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}}
O Opportunity fields (the ticket record)
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)
Merge keys
GHL auto-generates each merge key from the field name (lowercased; spaces and symbols become underscores - a "/" becomes a double underscore). The keys above assume these exact names; after creating the fields, open each one and confirm its real key before using it in emails and workflows.
Tip
Also create two Custom Values: company_name = your business name and support_sla = e.g. "24-48 hours", so the emails stay brand-driven and the SLA changes in one place.
Step 3 · The ticket number
A unique ID per ticket
A Recommended - date-time ID (zero dependency)
In the New sequence, set the ticket number to a formatted timestamp, e.g. TKT-{{right_now}} formatted to yyMMdd-HHmmss → TKT-260617-143052. Unique to the second; no counter needed. If two people submit in the same second, append the last digits of the contact id for an absolute guarantee. Uses GHL's date-field formatter - confirm the yyMMdd-HHmmss tokens render in a quick test before relying on them.
B Optional - sequential (TKT-1001, 1002…)
Keep a Custom Value counter and use a Math-Operation step (counter +1, write back, stamp the field). Only viable if this account's Math action can target a Custom Value - otherwise stay on the date ID. Either way the number is fully defined.
Step 4 · The support form
The one piece built by hand
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.
1
New FormCreate it
Sites → Forms → Builder → + Add Form
Name it exactly Submit a Support Ticket - the New sequence's trigger matches this name
2
Standard FieldsWho's contacting you
First Name + Last Name + Email (required), Phone (optional)
3
Custom FieldsDrag in the existing fields
Search each by name and drag it in - dropdown options auto-load from the field
Request Category (required) · Support Type (required) · Describe Your Issue (required, multi-line)
Attachment URL (optional, for a Loom / Drive / screenshot link)
4
File UploadLet them attach a screenshot
Add a File Upload field (optional); allow images / PDF / short video. Stored on the contact, surfaced in the team notification.
5
On-SubmitReassure them
Show message: "Thanks - your ticket is in. Our team will respond within {{custom_values.support_sla}}, and a confirmation email is on its way." (or redirect to a thank-you page)
6
Style + EmbedBrand it and ship it
Match your brand button color, then Save → Integrate → embed on a /support page or wherever customers reach support
Request Category options (example)
Billing & payments
Technical issue / bug
Account & access
Onboarding & setup
Product question
Other
Swap these for your own products, departments, or service lines - the router reads whatever you put here.
Support Type options (example)
Booking or reschedule
Payment or billing
Login / access
Feature or how-to
General question
Other
A second axis so the team can triage by intent, independent of category.
Step 5 · Email templates
Four branded emails
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.
New seq - on submitEmail 1 - Ticket Received · We've got it - Support Ticket #{{opportunity.ticket_number}}
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
Awaiting Reply seqEmail 2 - Additional Info Needed · One quick thing on Ticket #{{opportunity.ticket_number}}
Hi {{contact.first_name}},
We're working on Ticket #{{opportunity.ticket_number}} and need one more thing from you to move it forward:
What we need from you
{{opportunity.ticket_additional_info_needed}}
Just reply to this email with that and we'll pick it right back up.
Good news - Ticket #{{opportunity.ticket_number}} has been resolved.
Resolution
{{opportunity.ticket_resolution}}
If anything still isn't right, just reply to this email and we'll reopen it. Thanks for your patience.
Awaiting Reply seq - after 3 daysEmail 4 - Reminder · Still need a quick reply - Ticket #{{opportunity.ticket_number}}
Hi {{contact.first_name}},
We're holding Ticket #{{opportunity.ticket_number}} open and still waiting on one thing from you:
What we need from you
{{opportunity.ticket_additional_info_needed}}
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.
Reusable HTML shell (Gmail-safe - paste into GHL, swap the copy)
Acme.
Hi Jordan,
Thanks for reaching out. We've received your request and opened Support Ticket #TKT-260617-143052.
Our team is on it and will get back to you within 24-48 hours.
Need to add something? Just reply to this email and it'll attach straight to your ticket.
- The Acme Support Team
Acme • Support
Sample brand & copy shown - swap [Company] and the body for your own.
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)
Why a router, not a pipeline per category
One Support pipeline keeps every ticket on one board the team works from. The category drives the tag (so the board stays sortable + a category-specific step can fire), while the stage drives the customer emails. If a single category later needs heavier automation (e.g. auto-grant access, or a dev-team handoff), split that branch out into its own standalone workflow triggered by its tag - the engine doesn't change.
Step 7 · One automation per stage
Six workflows, each editable on its own
Every stage is its own workflow, triggered by Opportunity Status Changed filtered to this pipeline + stage (the New sequence is form-triggered, since the form is what creates the ticket). Drag a card to a stage and that stage's sequence fires - nothing overlaps. One cross-stage reply handler keeps the stages honest. Build each as a Draft, then publish after a test ticket runs clean. Click a workflow to expand its steps.
flowchart TB
F(["Form: Submit a Support Ticket"]):::form
subgraph NEW["NEW - trigger: Form Submitted"]
direction TB
n1["1 - Set ticket number TKT-yyMMdd-HHmmss"]:::new --> n2["2 - Create Opportunity (always new) into New"]:::new
n2 --> n3["3 - Copy contact fields into opportunity"]:::new
n3 --> n4["4 - Send Email 1 (Ticket Received)"]:::mail
n4 --> n5["5 - Category router: tag + notify team"]:::route
end
subgraph URG["URGENT - trigger: Stage to Urgent"]
direction TB
u1["1 - Tag priority-urgent"]:::urg --> u2["2 - Alert team (email + SMS)"]:::urg --> u3["3 - Task, due today"]:::urg
end
subgraph PROG["IN PROGRESS - trigger: Stage to In Progress"]
direction TB
p1["1 - Clear urgent tag"]:::prog --> p2["2 - Assign owner + work task"]:::prog --> p3["3 - Optional 'we're on it'"]:::prog
end
subgraph WAIT["AWAITING REPLY - trigger: Stage to Awaiting Reply"]
direction TB
w1["1 - Send Email 2 (info needed)"]:::mail --> w2["2 - Wait 3d, then Email 4 reminder"]:::wait
w2 --> w3["3 - Wait 2d, then auto-close"]:::wait
end
subgraph DONE["RESOLVED - trigger: Stage to Resolved"]
direction TB
d1["1 - Stamp Date Resolved"]:::done --> d2["2 - Send Email 3 (Resolved)"]:::mail --> d3["3 - Optional CSAT"]:::done
end
subgraph REPLY["REPLY handler - trigger: Customer Replied"]
direction TB
r1{"Current stage?"}:::wf -->|Awaiting Reply| r2["Move to In Progress + notify"]:::prog
r1 -->|Resolved| r3["Reopen to In Progress + notify"]:::prog
r1 -->|else| r4["Notify: new reply"]:::prog
end
F --> NEW
NEW -.->|drag: priority| URG
NEW -.->|drag: start work| PROG
URG -.-> PROG
PROG -.->|fill Info Needed| WAIT
PROG -.->|fill Resolution| DONE
WAIT -.->|timeout| DONE
REPLY -.->|reactivate / reopen| PROG
classDef form fill:#FFF1E6,stroke:#FF7A1A,color:#0B1E3F
classDef wf fill:#0B1E3F,stroke:#0B1E3F,color:#FFFFFF
classDef route fill:#FFF1E6,stroke:#FF7A1A,color:#0B1E3F,stroke-dasharray:4 3
classDef mail fill:#FFF4EC,stroke:#FFA155,color:#0B1E3F
classDef new fill:#EEF1F6,stroke:#5A6479,color:#0B1E3F
classDef urg fill:#FBEAE8,stroke:#D2544A,color:#0B1E3F
classDef prog fill:#FFF1E6,stroke:#FF7A1A,color:#0B1E3F
classDef wait fill:#FAF3E4,stroke:#C98A2E,color:#0B1E3F
classDef done fill:#EAF4EE,stroke:#3FA66A,color:#0B1E3F
1
New sequenceNew
Trigger: Form Submitted - Form is "Submit a Support Ticket"
1
Update Contact FieldGenerate the ticket number
Field: Support Ticket Number
Value: type TKT- then insert {{right_now}} → set date format yyMMdd-HHmmss → e.g. TKT-260617-143052
Notification subject: New ticket {{opportunity.ticket_number}} - {{opportunity.ticket_category}} / {{opportunity.ticket_support_type}}
The only stage a ticket enters automatically. Build order matters: number → opportunity → copy fields → email → router. Everything below is manual-drag triggered.
Title: URGENT: work ticket {{opportunity.ticket_number}} | Due: today
No customer email - Urgent is internal priority only. Agent SOP: drag any New / In Progress ticket here to escalate.
3
In Progress sequenceIn Progress
Trigger: Opportunity Status Changed - Pipeline "Support Tickets", Stage = In Progress
1
Remove Contact TagClear stale state
Remove priority-urgent (it's now actively worked, not just flagged)
2
Add TaskHand it to an owner
Title: Work ticket {{opportunity.ticket_number}} - {{opportunity.ticket_support_type}} | Due: +1 day
3
Send Email - optional"We're on it" touch
Off by default to avoid over-emailing. Turn on only if tickets tend to sit before pickup.
This is also where the Reply handler lands a reactivated/reopened ticket, so the owner is re-tasked automatically. 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.
Condition: the contact has an opportunity in Support Tickets (most reliable) - or list the specific support- tags individually (GHL filters don't match tag wildcards) - so a normal sales reply doesn't trip this workflow
2
If / ElseBranch on the ticket's current stage
If Awaiting Reply → move to In Progress + notify: Customer replied with the info
Else-if Resolved → move to In Progress + notify: Ticket reopened
Else → notify only: New reply on the ticket
GHL note: if a contact has more than one open opportunity, confirm the action targets the Support opportunity (filter by pipeline) so a reply doesn't move the wrong card.
Upgrade · One open ticket at a time
One open ticket per customer
By default a customer can open several tickets and each stays isolated on its own opportunity. But in GHL the routing state - the priority tag, the awaiting-reply timer - lives on the contact, not the opportunity. So two of the same person's tickets moving through stages at once can cross-interfere. The clean fix: let a customer hold one open ticket at a time, and politely bounce a second submission until the first is resolved.
Why a gate
GHL opportunities can't carry tags, and a workflow enrolls the contact. So the priority-urgent tag and the Awaiting-Reply timer are contact-level - if one person has two live tickets, clearing one can clear the other. Enforcing one open ticket sidesteps that whole class of problem instead of fighting GHL's data model.
flowchart TD
F["FORM: Submit a Support Ticket"]:::form --> G{"Has an open ticket? contact tag: has-open-ticket"}:::wf
G -->|Yes| B["Email 5 - Ticket Already Open STOP - no new card"]:::mail
G -->|No| A["Tag: ticket-approved"]:::route
A --> NEW["NEW sequence: clear ticket-approved, add has-open-ticket, open the ticket, route to the team"]:::act
NEW --> FLOW["ticket flows through the pipeline"]:::snew
FLOW --> R["RESOLVED sequence also removes has-open-ticket"]:::done
R -.->|customer can now open a new one| F
classDef form fill:#FFF1E6,stroke:#FF7A1A,color:#0B1E3F
classDef wf fill:#0B1E3F,stroke:#0B1E3F,color:#FFFFFF
classDef route fill:#FFF1E6,stroke:#FF7A1A,color:#0B1E3F,stroke-dasharray:4 3
classDef mail fill:#FFF4EC,stroke:#FFA155,color:#0B1E3F
classDef act fill:#FFFFFF,stroke:#FF7A1A,color:#0B1E3F
classDef snew fill:#EEF1F6,stroke:#5A6479,color:#0B1E3F
classDef done fill:#EAF4EE,stroke:#3FA66A,color:#0B1E3F
+
Intake gatefront door
Trigger: Form Submitted - Form is "Submit a Support Ticket"
1
If / ElseDoes this contact already have an open ticket?
Condition: contact has taghas-open-ticket
Use a tag condition, not a custom-field condition - GHL renders tag conditions natively (a custom-field condition shows a raw field id in the picker) and "has tag X" is the idiomatic check
2
Yes branch - Send EmailBounce the duplicate
Send 05 - Ticket Already Open (below), then the branch ends - no opportunity is created
3
No branch - Add Contact TagApprove it
Add tag ticket-approved - the hand-off that starts the New sequence
Why a separate workflow? It keeps the New sequence's steps untouched - the gate just decides whether to start it. A GHL If/Else branch can't "continue past" into shared steps, so wrapping the whole New sequence inside one branch would mean rebuilding it; a front-door gate avoids that.
T The open-ticket flag, start to finish
When
Workflow
Tag action
Form submitted, no open ticket
Intake gate
add ticket-approved
Ticket created
New sequence (first + after open)
remove ticket-approved, add has-open-ticket
Ticket resolved (or auto-closed)
Resolved sequence
remove has-open-ticket
Form submitted, ticket still open
Intake gate
no tag - sends the "already open" email and stops
One trigger change: with the gate on, the New sequence no longer triggers on the form directly - it triggers on Contact Tag added = ticket-approved (the gate owns the form). Its first step removes ticket-approved so the next ticket can re-add it.
Escape hatch
A flag only clears on Resolved, so a ticket parked forever in New would lock the customer out. The 5-day Awaiting-Reply auto-close covers anything that reaches Awaiting; for a ticket marked Lost or left in New, the agent (or a small "on Lost → remove has-open-ticket" workflow) clears it. Keep the tag removable so support can always unlock a customer by hand.
Intake gate - duplicateEmail 5 - Ticket Already Open · You already have an open ticket - #{{contact.support_ticket_number}}
Hi {{contact.first_name}},
Thanks for reaching out. You already have an open support ticket with us - #{{contact.support_ticket_number}} - and our team is on it.
To keep everything in one place, we'll reply to that existing ticket. There's no need to submit another - just reply to its confirmation email if you have more to add.
- The {{custom_values.company_name}} Support Team
Important setting
Cross-pipeline handling
A customer is often already an opportunity in another pipeline - a lead, a buyer, a subscriber. GHL models a person on two pipelines as one contact with multiple opportunities, so:
Enable "Allow Duplicate Opportunity" in the Create Opportunity action. Without create-new, opening a support opportunity for someone who already has one elsewhere is blocked - or it hijacks that existing opportunity into Support, corrupting their position in your sales pipeline.
New sequence's Create Opportunity = "always create new" in the Support pipeline, so a ticket never moves their other opportunity, and each ticket is its own record.
Keep contact dedup ON (match by email/phone). You want the same contact so the agent sees the customer's history - do not allow duplicate contacts, which splits one person into two and hides their history.
Edge cases
All covered
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 gate bounces it with the "already open" email - one open ticket per customer (optional upgrade)
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
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
Date-time-to-the-second (optionally + contact-id tail); sequential counter is the optional upgrade
Make it yours
Customize & extend
Quick swaps
Category list + tags in the router to match your products / departments
SLA in support_sla (same-day, 24-48 hrs)
Support Type options as your offer mix changes
Voice of the four emails to match your brand
Optional add-ons
A CSAT one-question survey after Resolved
A self-serve deflection (link a help center before a ticket opens)
SLA-breach alert if New / In Progress sits unworked past X hours
A /support page embedding the form
Ship it
Build checklist
Create the Support Tickets pipeline + 5 stages
Create the contact + opportunity custom fields, plus the company_name and support_sla custom values
Build the Submit a Support Ticket form (Request Category + Support Type dropdowns)
Build the 4 email templates from the copy above (From + Reply-To = your support inbox)
Enable Allow Duplicate Opportunity in the Create Opportunity action
Build the 6 stage workflows as Drafts: one per stage + the cross-stage reply handler. The category router is an If/Else branch inside the New sequence. (The optional Intake gate is a 7th workflow - see below.)
Optional - one open ticket per customer: add the has-open-ticket + ticket-approved tags and the 05 - Ticket Already Open email; build the Intake gate workflow; switch the New sequence trigger to Tag added = ticket-approved (New adds has-open-ticket, Resolved removes it)
Turn on Allow Re-Entry on the stage workflows (especially In Progress) so reopened / reactivated tickets re-fire their sequence
Test: submit a ticket per category → confirm ID + opportunity + Email 1 + team pinged; drag through every stage → Emails 2 & 3; reply to confirm the reply handler reactivates. Then publish.
The mindset
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.