Typestate
Sign in

Support tickets

Each message from your support form becomes a numbered ticket, and the answers go by email.

For a shop or a software maker with a support form.

Resend logo Resend
Start it with your agentStarting from a blueprint does not count against the action changes of your plan.

Start the support tickets blueprint on Typestate

Asked you for the address of your support page and your team's address and the address your emails come from.

Started Support tickets: 3 record types, 6 web endpoints.

8 actions are compiling.

Deployed to Sandbox. Try it at your Sandbox address.

What your agent does when you start this blueprint

What runs

Each step has 3 parts: who starts it, how it reaches your backend, and the action that answers.

  1. A customerwrites from the form on your site
    Web endpointPOST /tickets
    Open a TicketOpens a numbered ticket, emails the customer their link and tells the team.saves a Ticketsaves a Replysends 2 emails
  2. Your teamanswers the ticket
    Web endpoint, your keyPOST /tickets/reply
    Reply to a TicketKeeps the answer and emails it to the customer.saves a Replychanges a Ticketsends 1 email
  3. The customeranswers back from their link
    Web endpointPOST /tickets/answer
    Add to My TicketOpens the ticket again for the team.saves a Replychanges a Ticket
  4. The backendnames the tickets that wait more than a day
    TimerEvery hour at 00 past
    Chase Unanswered TicketsSends the team one email with their numbers.changes Ticketssends 1 email
  5. Your teammarks the ticket solved
    Web endpoint, your keyPOST /tickets/update
    Update a TicketChanges its state, or gives it to a person.changes a Ticket
  6. The backendcloses it a week later
    TimerEvery day at 03:00, Berlin time
    Close Solved TicketsCloses each ticket solved 7 days ago or earlier.changes Tickets
  • Web endpoint: your website or app calls it
  • Timer: it runs at a set time

The rule you will want to change first

A ticket whose customer waited more than 24 hours is named to the team, once, until the customer writes again.

It lives in the action Chase Unanswered Tickets. Tell your agent how you want it instead: one sentence changes, and you read the change before it goes Live.

What it is made of

The map shows the records it keeps, the actions it runs and the services it calls.

What it keeps

Customeremail, name, code
Ticketnumber, customer email, subject, state, assignee, last customer at, chased, solved at
Replyticket, author, body

What it does

Open a Ticketweb endpoint
Reply to a Ticketweb endpoint, your key
Add to My Ticketweb endpoint
Update a Ticketweb endpoint, your key
Read My Ticketsweb endpoint
List Ticketsweb endpoint, your key
Chase Unanswered TicketsEvery hour at 00 pastThe rule to change first
Close Solved TicketsEvery day at 03:00, Berlin time

What it reaches

ResendSend email from your own address: sign-in links, receipts, reminders.
Your website6 web endpoints

What it keeps

Customer

3 fields

One person who wrote to support, with the code in their own link.

email
required, an email address, never twice
name
optional, at most 200 characters
code
required, never twice, at most 12 characters

A customer is saved when they first write. The actions find them by their code.

Ticket

8 fields

One question from a customer, with its number, its state and who has it.

number
required, a whole number, never twice
customer email
required, an email address
subject
required, at most 200 characters
state
required, one of open, waiting, solved, closed
assignee
optional, at most 200 characters
last customer at
required, a date and time
chased
required, yes or no
solved at
optional, a date and time

A customer opens and answers tickets through the actions. Only your key deletes one.

Reply

3 fields

One message on a ticket, from the customer or from the team.

ticket
required
author
required, one of customer, team
body
required, at most 10000 characters

A customer adds messages through the actions. Only your key reads them all.

What it does

Open an action to read its steps: each sentence, with the calls that do it under it.

Open a TicketA customer writes to you from the form on your site, and gets a ticket number and their own link.

Who starts it

From the webPOST /tickets

What it is given

EmailEmail, needed

Nametext, optional

Subjecttext, needed

Messagetext, needed

What it does

  • input.emailwhat the caller gives
  • customera value it works out
  • Customera record type
  • Customeran attribute of a record or an answer
  • "customer"a fixed text or number
  • nowwhat it knows without being told
  • toa part of an email or a payment

Find the Customer whose email is the given email.When none matches, create a Customer with the given email and name, and a new code of 12 random letters and digits.The customer's code is that Customer's code, or the new code.

customer is

Find the first Customer where

email is input.email

when none carry on without one

new_code is a random code of 12 letters and digits

When customer holds no value

Add a Customer

set code to new_code

set email to input.email

set name to input.name

code is the customer's code, or else the value named new code

The ticket's number is the count of Tickets plus one.

tickets is

Count all Tickets

number is tickets + 1

Create a Ticket with that number, customer_email set to the given email, subject set to the given subject, and last_customer_at set to now.Create a Reply for that Ticket with author set to customer and body set to the given message.

Add a Ticket

set customer_email to input.email

set last_customer_at to now

set number to number

set subject to input.subject

Add a Reply

set author to "customer"

set body to input.message

set ticket_id to ticket.id

Email the given email with the subject "We got your message", saying the ticket's number and the customer's own link, which is the SUPPORT_ADDRESS setting with ?code= and the customer's code on the end.

Send an email

to input.email

template "Open a Ticket Email"

code code

number number as a number

value the setting SUPPORT_ADDRESS

Email the TEAM_EMAIL setting with the subject "New ticket", saying the ticket's number, the given subject and the given email.

Send an email

to the setting TEAM_EMAIL

template "New Ticket Email"

email input.email

number number as a number

subject input.subject

Answer with the ticket's number.

Answer

number is number

Reply to a TicketYour team answers a ticket, and the customer gets the answer by email.

Who starts it

From the webPOST /tickets/reply

What it is given

Numbera whole number, needed

Messagetext, needed

What it does

  • input.numberwhat the caller gives
  • ticketa value it works out
  • Ticketa record type
  • Ticketan attribute of a record or an answer
  • "closed"a fixed text or number
  • toa part of an email or a payment
  • refusewhere it says no

Find the Ticket whose number is the given number.When none matches, refuse with "No such ticket".

ticket is

Find the first Ticket where

number is input.number

when none refuse "No such ticket"

When that Ticket's state is closed, refuse with "This ticket is closed".

When ticket.state is "closed"

Refuse "This ticket is closed"

Find the Customer whose email is that Ticket's customer_email.When none matches, refuse with "No customer matches this ticket".

customer is

Find the first Customer where

email is ticket.customer_email

when none refuse "No customer matches this ticket"

Create a Reply for that Ticket with author set to team and body set to the given message.Change that Ticket's state to waiting.

Add a Reply

set author to "team"

set body to input.message

set ticket_id to ticket.id

Change the Ticket where

id is ticket.id

set state to "waiting"

Email that Ticket's customer_email with the subject "An answer to your ticket", saying the ticket's number, the given message, and the customer's own link, which is the SUPPORT_ADDRESS setting with ?code= and that Customer's code on the end.

Send an email

to ticket.customer_email

template "Reply to a Ticket Email"

code customer.code

message input.message

number ticket.number as a number

value the setting SUPPORT_ADDRESS

Answer with that Ticket's number, and waiting as the state.

Answer

number is ticket.number

state is "waiting"

Add to My TicketA customer answers on their ticket from their own link.

Who starts it

From the webPOST /tickets/answer

What it is given

Codetext, needed

Numbera whole number, needed

Messagetext, needed

What it does

  • input.codewhat the caller gives
  • customera value it works out
  • Customera record type
  • Customeran attribute of a record or an answer
  • "closed"a fixed text or number
  • nowwhat it knows without being told
  • refusewhere it says no

Find the Customer whose code is the given code.When none matches, refuse with "No tickets match this link".

customer is

Find the first Customer where

code is input.code

when none refuse "No tickets match this link"

Find the Ticket whose number is the given number and whose customer_email is that Customer's email.When none matches, refuse with "No tickets match this link".

ticket is

Find the first Ticket where

customer_email is customer.email

number is input.number

when none refuse "No tickets match this link"

When that Ticket's state is closed, refuse with "This ticket is closed. Open a new one".

When ticket.state is "closed"

Refuse "This ticket is closed. Open a new one"

Create a Reply for that Ticket with author set to customer and body set to the given message.Change that Ticket's state to open, its last_customer_at to now, and its chased to false.

Add a Reply

set author to "customer"

set body to input.message

set ticket_id to ticket.id

Change the Ticket where

id is ticket.id

set chased to false

set last_customer_at to now

set state to "open"

Answer with that Ticket's number, and open as the state.

Answer

number is ticket.number

state is "open"

Update a TicketYour team assigns a ticket to a person, or changes its state.

Who starts it

From the webPOST /tickets/update

What it is given

Numbera whole number, needed

Stateone of open, waiting, solved, optional

Assigneetext, optional

What it does

  • input.numberwhat the caller gives
  • ticketa value it works out
  • Ticketa record type
  • Ticketan attribute of a record or an answer
  • "closed"a fixed text or number
  • refusewhere it says no

Find the Ticket whose number is the given number.When none matches, refuse with "No such ticket".

ticket is

Find the first Ticket where

number is input.number

when none refuse "No such ticket"

When that Ticket's state is closed, refuse with "This ticket is closed".

When ticket.state is "closed"

Refuse "This ticket is closed"

Change that Ticket's state to the given state and its assignee to the given assignee, and keep each one that is not given.When the given state is solved, set its solved_at to now.

new_state is the given state, or else the ticket's state

Change the Ticket where

id is ticket.id

set assignee to the given assignee, or else the ticket's assignee

set solved_at to now when the given state is "solved", otherwise the ticket's solved at

set state to new_state

Answer with that Ticket's number, its state and its assignee.

Answer

assignee is changed.assignee

number is ticket.number

state is changed.state

Read My TicketsA customer reads their own tickets from their own link.

Who starts it

From the webGET /tickets/mine

What it is given

Codetext, needed

What it does

  • input.codewhat the caller gives
  • customera value it works out
  • Customera record type
  • Customeran attribute of a record or an answer
  • refusewhere it says no

Find the Customer whose code is the given code.When none matches, refuse with "No tickets match this link".

customer is

Find the first Customer where

code is input.code

when none refuse "No tickets match this link"

List the Tickets whose customer_email is that Customer's email, newest first.

tickets is

Find Tickets where

customer_email is customer.email

sorted by created_at, highest first

For each Ticket, answer its number, subject, state, and created_at as the time it was opened.

rows is

each ticket of tickets as

number is ticket.number

opened_at is ticket.created_at

state is ticket.state

subject is ticket.subject

Answer

tickets is rows

List TicketsYour team reads the tickets, the longest waiting first.

Who starts it

From the webGET /tickets

What it is given

Stateone of open, waiting, solved, closed, optional

What it does

  • input.statewhat the caller gives
  • ticketsa value it works out
  • Ticketa record type
  • Ticketan attribute of a record or an answer

List the Tickets, the oldest last_customer_at first.When a state is given, list only the Tickets in that state.

tickets is

Find Tickets where

state is input.state

sorted by last_customer_at, lowest first

For each Ticket, answer its number, customer_email, subject, state, assignee and last_customer_at.

rows is

each ticket of tickets as

assignee is ticket.assignee

customer_email is ticket.customer_email

last_customer_at is ticket.last_customer_at

number is ticket.number

state is ticket.state

subject is ticket.subject

Answer

tickets is rows

Chase Unanswered TicketsEvery hour, the team gets one email that names each ticket a customer has waited on for more than 24 hours.

Who starts it

A timer, every hour at 00 past

What it is given

Nothing. It needs no input.

What it does

  • ticketsa value it works out
  • Ticketa record type
  • Ticketan attribute of a record or an answer
  • falsea fixed text or number
  • nowwhat it knows without being told
  • toa part of an email or a payment

List the Tickets whose state is open, whose chased is false, and whose last_customer_at is 24 hours ago or earlier.

tickets is

Find Tickets where

chased is false

last_customer_at is on or before now - 24 hours

state is "open"

For each of those Tickets, change its chased to true, and keep its number for the email.

numbers is an empty list

For each ticket in tickets

Change the Ticket where

id is ticket.id

set chased to true

Adds the ticket's number as the text of a number to numbers.

When there is at least one such Ticket, email the TEAM_EMAIL setting with the subject "Tickets waiting for an answer", saying the count of those Tickets and their numbers.

When number of numbers is more than 0

Send an email

to the setting TEAM_EMAIL

template "Chase Unanswered Tickets Email"

count the number of the value named numbers as a number

numbers the items of the value named numbers joined with ", "

Close Solved TicketsEvery night, a ticket solved 7 days ago or earlier is closed.

Who starts it

A timer, every day at 03:00, Berlin time

What it is given

Nothing. It needs no input.

What it does

  • ticketsa value it works out
  • Ticketa record type
  • Ticketan attribute of a record or an answer
  • 7 daysa fixed text or number
  • nowwhat it knows without being told

List the Tickets whose state is solved and whose solved_at is 7 days ago or earlier.

tickets is

Find Tickets where

solved_at is on or before now - 7 days

state is "solved"

For each of those Tickets, change its state to closed.

For each ticket in tickets

Change the Ticket where

id is ticket.id

set state to "closed"

What it does by itself

  • Every hour, the team hears of the tickets that wait more than 24 hours.
  • Every night at 03:00, Berlin time, tickets solved a week ago are closed.

Who signs in and what they may read

People sign up and sign in with an email address and a password, or with a provider you add, such as Google.

  • Customer: A customer is saved when they first write. The actions find them by their code.
  • Ticket: A customer opens and answers tickets through the actions. Only your key deletes one.
  • Reply: A customer adds messages through the actions. Only your key reads them all.

Web endpoints

What it doesWho may callAddress
Your support form posts here.anyonePOST /tickets
A customer answers on a ticket from their own link.anyonePOST /tickets/answer
A customer reads their tickets from their own link.anyoneGET /tickets/mine
Only your team, with your key, answers a ticket.you, with your keyPOST /tickets/reply
Only your team, with your key, assigns a ticket or changes its state.you, with your keyPOST /tickets/update
Only your team, with your key, reads the tickets.you, with your keyGET /tickets

What it cannot do yet

  • It does not read your mailbox. A customer writes through the form on your site, and answers through the link in each email.
  • It does not build the pages. Your site shows the form and the customer's tickets, and posts to it.
  • The customer's link opens their tickets without a password. Anybody who has the link reads them.

Build the pages with your tool

Typestate is the backend. Paste this brief into Lovable, v0, Claude Code or the tool that builds your pages.

Show the brief
Build the pages for "Support tickets". The backend runs on Typestate and answers these web endpoints: POST /tickets: Your support form posts here. Who may call: anyone. POST /tickets/answer: A customer answers on a ticket from their own link. Who may call: anyone. GET /tickets/mine: A customer reads their tickets from their own link. Who may call: anyone. POST /tickets/reply: Only your team, with your key, answers a ticket. Who may call: you, with your key. POST /tickets/update: Only your team, with your key, assigns a ticket or changes its state. Who may call: you, with your key. GET /tickets: Only your team, with your key, reads the tickets. Who may call: you, with your key. People sign up at POST /auth/signup and sign in at POST /auth/login, which answers a token. Send it as Authorization: Bearer <token>. A refused call answers a sentence that can be shown to the person as it is.

Start it

  1. Tell your agent the sentence on the right.
  2. It asks you for the https address of the page on your site where a customer reads their tickets, such as https://www.example.com/support.
  3. It asks you for the address of your team, which gets each new ticket and the tickets that wait.
  4. It asks you for the address emails come from, on a domain you verified at Resend, such as "Acme Support <support@acme.example>".
  5. It tells you what only you can do, such as pasting your Resend key in the cockpit. A key never goes through the agent.
  6. Open your Sandbox address and try it yourself.

Starting from this blueprint does not count against the action changes of your plan. The Free plan runs it on Sandbox. Live needs a paid plan.

Start the support tickets blueprint on TypestateChoose a plan

Related blueprints