Landscapt (CRM)

Tickets

Support and service tickets — where they come from, how they're worked, and how they connect to the rest of a client's record.

What a ticket is

A ticket (crm_tickets) is Landscapt's general-purpose customer-service record — a note, call, or event tied to a client that needs to be tracked, assigned, and resolved. Every ticket has a sequential ticket_number, a status, a priority, an optional category, and an optional client and assignee.

Tickets are shown as three different lists in the app — Tickets, Calls, and Events — but they're all the same underlying table and component (TicketsList), just filtered by type (note / call / event).

Not the same as CMMS Maintenance Requests

Tickets (this page) are Landscapt/CRM's customer-service record — a client asks something, reports something, or needs a callback. Maintenance Requests are a separate, module-specific Equipt/CMMS concept for reporting an asset or equipment problem that may become a Work Order. They live in different tables, have different fields, and are not interchangeable — a client complaint is a Ticket; a broken mower is a Maintenance Request.

Status, priority & type reference

These are literal string enums, not free text — the UI only ever writes one of these.

StatusValueMeaning
OpenopenDefault status on every new ticket — including every ticket a public form creates.
On Holdon_holdWaiting on something outside your control (a callback, a part, a client reply). Counts as the same “Open” stage on the status flow indicator.
PendingpendingBeing actively worked — the middle step on the status flow (“In Progress”).
ClosedclosedResolved. Sets closed_at and fires the Ticket Closed trigger (Zapier + internal automations).

The detail sheet's status flow indicator only shows three visual steps — Open, In Progress, Closed — because on_hold maps to the same step index as open. Under the hood it's still a distinct, filterable status.

PriorityNotes
LowShown with no badge in the list view — only High and Urgent get a colored pill.
NormalThe default on every new ticket, including form-created ones.
HighOrange badge in the list.
UrgentRed badge in the list.
TypeNotes
NoteThe default type — a written ticket. What Forms always create.
CallSame record shape, filtered into a dedicated Calls list (typeFilter="call") elsewhere in the app.
EventSame record shape, filtered into an Events list (typeFilter="event").

Category is a free-form org-configured list (crm_list_options under key ticket_categories), not a hard-coded enum. If your org hasn't configured any, the New Ticket dialog and edit form fall back to a default set: Uncategorized, Estimate, Billing, Change Service, Complaint, Other.

Creating a ticket

Three ways a ticket comes into existence:

  1. Staff, manually — the Add Ticket (or Add Call / Add Event, depending on which list you're on) button opens NewTicketDialog, letting staff set type, client, category, subject, body, status, priority, assignee, and due date directly.
  2. A public form submission, automatically — every submission on a crm_forms form creates a ticket, no matter what else the form does or how it handles the matched/created client. The ticket always starts as status: "open", priority: "normal", type: "note", with category set to the form's own name and the body built from the submitter's name/email/phone/message plus every raw field on the form.
  3. A Communication Automation Event action — the Ticket action step lets an automation sequence create a ticket with a configurable title, description, priority (Low/Normal/High/Urgent), and assignee whenever it runs.

Bulk creation is also available via CSV import (Actions menu → Import), which requires only a Subject column.

Assignment

A ticket can be assigned to exactly one user at a time, via assigned_to (a display-name string) and assigned_to_id (a real crm_employees.id, resolved from the assignee-picker's selected employee). Both are set together whenever the UI's Assigned To dropdown is used — in the New Ticket dialog, the edit form, or the list's bulk Reassign action (select one or more rows → Actions → Reassign).

resolveAssigneeId() in src/lib/ticket-notify.ts turns that crm_employees.id into a login user for notifications by looking up crm_employees.user_id. Tickets created before assigned_to_id existed, or created through a path that never had a real id (the public form path always leaves the assignee blank; the Zapier "Create Ticket" action has no assignee field either), only have the name string — resolveAssigneeId() falls back to fuzzy-matching that name against crm_employees so assignment notifications still work — but if the name doesn't match an employee exactly, assignment notifications are silently skipped rather than failing.

Worked example: a form-to-close lifecycle

  1. A visitor submits a contact form on your public site. submitFormResponse() matches or creates a client, then unconditionally inserts a crm_tickets row: open, normal priority, type note, category = the form's name, body = name/email/phone/ message plus every raw field.
  2. notifyStaffOfNewTicket() runs immediately — it notifies the org's configured New Ticket recipients (no assignee yet, so no one else is added).
  3. Because the ticket has a client, ticket_created fires for any Communication Automation sequence watching that trigger (optionally filtered to this form's category), and for any Zapier "New Ticket" trigger.
  4. A manager opens the ticket from the Tickets list, clicks Reassign (or edits the ticket directly) and assigns it to a rep. This sets both assigned_to and assigned_to_id, and fires a ticket_assigned notification to that rep alone.
  5. The rep works the ticket — adds comments (Comments & History tab, backed by CommentsSection), which notify the assignee if someone else comments, and sets status to Pending while they investigate.
  6. The rep resolves it and clicks Close Ticket (useCloseTicket). This sets status: "closed" and closed_at, then fires ticket_closed for both internal automations and Zapier.
  7. If the client later replies and staff change the status away from Closed, useUpdateTicket detects the prior status was closed and fires ticket_reopened.

The ticket detail sheet

Clicking any ticket row opens TicketDetailSheet, with five tabs:

  • Details — status flow buttons, the client/category/assignee/dates meta grid, body text, and a Linked Records picker that can attach the ticket to an Estimate, Invoice, or Job belonging to the same client.
  • Comments & History — threaded comments via the shared CommentsSection (recordType="ticket").
  • Files — attachments via the shared AttachmentsSection.
  • Contributors — additional staff CC'd on the ticket as participants, distinct from the single assignee. Managed by use-ticket-contributors.ts.
  • Audit Trail — the shared AuditTrailTab change history.

The header also has a PDF button that opens a print-formatted view in a new window — all ticket fields are HTML-escaped before being written into that window, since subject/body/names can originate from an unsanitized public form submission.

Field upsells from crews

When a crew spots work a property needs, they send it in from the crew app and it arrives here as an ordinary ticket with the category Upsell — so it inherits assignment, due dates, priority, comments, automations and the audit trail without being a separate thing to check.

The crew's side of this — where the Suggest work button lives and what they see — is in the Crew App guide. Each one carries the crew's note, who spotted it, the date, the property, and the service they suggested. Any photo they took is on the Files tab, which is usually enough to price the work without a trip out to look. The ticket is linked back to the job the crew was on.

Crews never see or set a price. They pick from a list you control and describe what they saw; pricing stays with the office. Open a service up for suggesting by ticking Show in field upsells on it under Services & Pricing, and give it a short prompt so crews know what to look for. With no services ticked, the Suggest work button doesn't appear in the crew app at all — that is how the feature is switched on and off for crews, with no permission to manage on their side.

To take one forward, click Create estimate on the ticket. That makes a draft estimate for the client, puts the suggested service on it at your catalog rate, links the two together and opens the estimate ready to price — so there's nothing to retype and nothing to remember to link afterwards. The button needs the Add Estimate permission, the same one every other way of creating an estimate requires, and it disappears once the ticket has been converted.

One ticket can only be converted once, even if two people click at the same moment or a conversion is interrupted half-way. The estimate itself claims the ticket, so a second attempt can't create a duplicate — it reopens the estimate that already exists, finishing any step the first attempt missed. If you convert a ticket by mistake, delete the estimate and the ticket becomes convertible again.

Re-quoting is still fine. You can link additional estimates by hand from Linked Records — useful if you quoted the work before the suggestion came in, or the client asked for a revised price. The reports read the most recent estimate linked to the ticket.

That link is what turns the suggestion into a number you can report on: the estimate carries the real sales pipeline (Sent → Accepted → Won/Lost), so a crew's suggestion reaching Won is measurable, while the ticket's own open/closed only ever means whether the office has dealt with it.

Two reports in the Report Center read this: Field Upsells lists every suggestion with its outcome, and Upsell Conversion by Crew Member totals what each person submitted against the revenue it won. Both are under the Service section, and each has its own permission so you can decide who sees them.

Automations, Zapier & notifications

Communication Automations can trigger on four ticket events:

TriggerFires when…
Ticket was createdFires when any ticket is created with a client attached — includes form-created tickets, since submit-form-response.ts links the ticket to the matched/created client.
Ticket was closedFires when a ticket's status changes to closed, from useCloseTicket or a bulk “Mark Closed” action.
Ticket was reopenedFires when an update changes status away from "closed" to anything else — i.e. the ticket's prior status was closed.
Ticket past dueNot a moment-it-happened event — evaluated against due_date via the ticket_past_due_days condition field (days since due date).

The same events are exposed to Zapier as instant triggers — New Ticket and Ticket Closed — and Zapier can also create tickets via the Create Ticket action (requires Client + Subject; accepts Body, Priority, Category, Type, and Due Date). See the Zapier guide for the full trigger/action reference.

Three notification preferences exist per user, each with separate email and in-app toggles:

PreferenceKeysWho receives it
New TicketinAppNewTicket / emailNewTicketSent to the org's configured “New Ticket Recipients” plus the resolved assignee (if any), excluding whoever created the ticket. Also fires for anonymous form submissions (no creator to exclude).
Ticket AssignedinAppTicketAssigned / emailTicketAssignedSent only to the newly-assigned user — skipped if they assigned it to themselves.
Ticket CommentinAppTicketComment / emailTicketCommentSent only to the resolved assignee — skipped if the assignee is the one commenting.

More in Landscapt (CRM)

Clients, Properties & Leads
Client accounts, commercial hierarchies, service properties, and the activity timeline that ties it all together.
Estimates & the Budget Engine
How an estimate is built, how its numbers are actually calculated, and how it becomes a job.
Jobs & Packages
The six job types, how a job's status differs from a visit's status, and how a Package template turns into a billed job.
Services & Pricing
The service catalog, bulk catalog price changes, and Price Adjustment runs — which prices seed new work and which ones actually bill.
The Dispatch Board
The daily scheduling screen crews and dispatchers live in — visits, crews, status, and how actual hours get calculated.
The Crew App
What a crew sees on their phone — the day's stops, clocking on and off, breaks, photos, and sending work back to the office.
Sales Meetings
Booking appointments per sales rep, the double-booking warning, and the split between a rep's direct reminder and the client-facing automation trigger.
The Waiting List
Jobs with a date range instead of a date, held for opportunistic scheduling — and how to actually get them dispatched.
Snow Jobs & Storm Dispatch
Storm events, priority dispatch, and the invoicing flow built specifically for snow.
Invoicing & Payments
How an invoice is born, how its status moves, and where the client's own PO number actually goes.
Contracts
Ongoing billing agreements — monthly amounts, seasonal overrides, sub-properties, and how signing and invoicing actually work.
Communication Automations
How sequences, triggers, and events work — and how Automations differ from Sales Campaigns.
Forms & Lead Capture
Building, publishing, and sharing public forms — and what happens to a submission once it lands.
Damage Cases
Tracking property damage and warranty claims tied to a job — what a case captures, how cost rolls up, and a real current limitation in how it connects to a client record.
Job Photos
Field photo documentation, annotation, and before/after comparisons — attached to a job site, not a person.
Online Payments & Stripe
How a client actually pays an invoice online, how Stripe Connect keeps every org's money in their own account, and how the platform takes zero cut.
Browse all guides
© 2026 Landscapt. All rights reserved.