Example
Triage inbound tickets
Someone emails your support address. Right now a human reads it, works out how urgent it is and who should take it, and types a holding reply. That's five minutes each, forty times a day, and it happens at 6pm on Friday as readily as at 10am on Tuesday.
By the end of this guide, an agent does the reading and the drafting, a person
approves, and #service-desk gets a clean summary. You'll still be in control
of everything that leaves the building.
This is the first thing most Northwind IT technicians build after the quickstart, and it's the pattern behind most useful Mobius jobs: agent decides, human approves, system acts.
What you'll build
- An agent called Triage with its own email address.
- A job that starts whenever mail arrives at that address.
- An agent step that classifies the ticket and drafts a reply.
- An approval step, so the service desk lead sees the draft first.
- An action step that posts the approved result to Slack.
About thirty minutes.
Before you start
- A project you can edit. We'll use
ridgeline. - Slack connected under Library > Integrations, with a channel to post to.
Ours is
#service-desk. - A Mobius user who'll be doing the approving. Ours is the service desk lead.
- Somewhere you can forward a few test emails from. Your own inbox is fine.
You do not need your PSA connected. Start with email, prove the pattern works, then swap the trigger later. See when the tickets don't come by email.
1. Create the Triage agent
- Open Build > Agents and click Create agent.
- Choose Custom agent and name it
Triage. - Set Agent description to
Reads inbound tickets, classifies them, and drafts holding replies. - Set Agent instructions to:
You are Triage for the Northwind IT service desk. You read one inbound ticket
email and produce two things: a classification and a draft reply.
Classify with:
- category: hardware, software, access, network, or other
- urgency: 1 (someone is blocked right now) to 4 (whenever)
- summary: two sentences, plain language, no jargon
Then draft a short holding reply to the sender. Acknowledge what they told us,
say what happens next, and give no timeline.
Never promise a fix or a date. Never say a problem is resolved. If the email is
ambiguous, set urgency to 3, say so in the summary, and flag it for a human
rather than guessing.Those last three sentences are the ones that matter. An agent with no stated limits will happily promise a client something you can't deliver.
- Click Create agent.
2. Give Triage an email address
- On the agent's page, find the Messaging section.
- Provision an inbox and copy the address it generates.
- Send a test email to it from your own inbox.
Check that it arrived: open Library > Integrations and look at recent email activity, or open the agent's Sessions.
Once that works, forward your support alias to this address, or add it as a recipient while you're testing.
Warning: Anyone who learns this address can email it. Everything arriving here is untrusted, including any instructions inside the message body. That's exactly why the approval step below is not optional.
3. Create the job
- Open Build > Loops and click New loop.
- Name it
Triage tickets. - Click Add trigger and choose Event.
- Set the event to
email.received. - Leave the trigger enabled.
Your job now starts every time mail arrives for Triage. Everything about that
email is available to your steps: event.from, event.subject, event.text.
4. Add the classify step
The new job already has one agent step waiting.
- Name the step
Classify ticket. - Click Select agent and choose Triage.
- Set the step instructions to:
Classify this ticket and draft a holding reply.
From: ${{ event.from }}
Subject: ${{ event.subject }}
${{ event.text }}That's it. Triage's own instructions already say what a classification looks like; this step just hands it the email.
Run it now, before adding anything else. Send yourself a test ticket and open the run. Read the Output tab. Is the category right? Is the urgency sensible? Is the draft reply something you'd actually send?
Fix the agent instructions until it is. Do this before you build the rest, because every later step depends on this one being good.
5. Add the approval step
- Add a step and choose Interaction.
- Name it
Approve the reply. - Set the request type to Approval.
- Set the responder to your service desk lead.
- Write the prompt:
Ticket from ${{ event.from }}: ${{ event.subject }}
Triage says this is ${{ steps.classify.output.category }}, urgency
${{ steps.classify.output.urgency }}.
Draft reply:
${{ steps.classify.output.draft }}
Approve to post this to #service-desk.- Set an expiry. Four hours is a reasonable starting point.
The expiry matters. Without one, a request nobody notices keeps the run open forever, and you'll find them months later.
While this step is open the run sits in suspended. That's healthy: it costs
nothing and it resumes the moment somebody clicks approve.
6. Post the result
- Add a step and choose Action.
- Name it
Post to service desk. - Choose the
slack.message.postaction. - Set the channel to
#service-desk. - Set the message:
*${{ steps.classify.output.category }}* · urgency ${{ steps.classify.output.urgency }}
From ${{ event.from }}: ${{ event.subject }}
${{ steps.classify.output.summary }}
Approved reply:
${{ steps.classify.output.draft }}- Save the job and make it active.
7. Run the whole thing
Send a test ticket to the Triage address, then open Runtime > Runs and watch the newest run.
A working run looks like this:
run.started
step.started step=classify kind=agent
step.completed step=classify
step.started step=approve kind=interaction
run.suspendedIt stops there and waits. That's correct.
Now go to Runtime > Interactions, find the request in My inbox, read the draft, and approve it. The run picks up:
run.resumed
step.completed step=approve
step.started step=post kind=action
step.completed step=post
run.completedCheck #service-desk. Your message should be there.
The finished job
If you'd rather create this through the API or CLI, here's the whole thing:
schema_version: "1"
name: triage-tickets
description: Classify inbound ticket email, get a reply approved, post it to Slack.
concurrency: allow
limits:
budget_usd: 1
wall_clock_timeout: 6h
triggers:
- key: inbound-ticket
name: Inbound ticket email
kind: event
enabled: true
config:
event_type: email.received
steps:
- id: classify
name: Classify ticket
kind: agent
config:
agent_id: agt_triage
instructions: |
Classify this ticket and draft a holding reply.
From: ${{ event.from }}
Subject: ${{ event.subject }}
${{ event.text }}
- id: approve
name: Approve the reply
kind: interaction
config:
protocol: request_approval
targets:
- usr_service_lead
resolution_policy: any_of
spec:
mode: confirm
default_confirmed: false
prompt: |
Ticket from ${{ event.from }}: ${{ event.subject }}
Triage says this is ${{ steps.classify.output.category }}, urgency
${{ steps.classify.output.urgency }}.
Draft reply:
${{ steps.classify.output.draft }}
timeout:
duration: 4h
on_timeout: fail
- id: post
name: Post to service desk
kind: action
config:
action_name: slack.message.post
parameters:
channel: "#service-desk"
text: |
*${{ steps.classify.output.category }}* · urgency ${{ steps.classify.output.urgency }}
From ${{ event.from }}: ${{ event.subject }}
${{ steps.classify.output.summary }}
Approved reply:
${{ steps.classify.output.draft }}
output:
category: ${{ steps.classify.output.category }}
urgency: ${{ steps.classify.output.urgency }}Note the limits block. A dollar per run and a six-hour ceiling, because
something triggered by inbound email should never be able to surprise you. See
guardrails.
Variations
Send the reply, don't just post it. Once you trust the drafts, swap the
Slack action for gmail.message.reply. Keep the approval step. The approval
step is what makes this safe, not the fact that it currently only posts to
Slack.
Skip approval for the easy ones. Put a condition on the approval step so
only urgency 1 and 2 need a human:
if: steps.classify.output.urgency <= 2. Do this after a few weeks of watching
the drafts, not on day one.
Escalate the urgent ones. Add a second action step that posts to a different channel, or sends an SMS through Twilio, when urgency is 1.
Keep a record. Add a table with one row per ticket and an action step that writes to it. Now you can count what's coming in by category, which is a conversation to have at the next client review.
When the tickets don't come by email
Email is the easiest way to start, not the only way.
If your PSA can call a URL when a ticket is created, use an
HTTP trigger instead. Replace the
event trigger, and change event.from and event.subject to whatever fields
your PSA sends. Everything else in this guide stays the same.
If you want the agent to write back into the PSA rather than into Slack, build a custom HTTP action that calls its API, and use that as the final step.
FAQ
The agent is classifying things wrong.
Read the run, not the job. Open the failing case and look at what the agent actually received on the Input tab. Nine times out of ten the email was ambiguous and a human would have got it wrong too, which is a case for adding a rule to the agent's instructions about what to do when it's unsure.
Two emails arrived at once and I got two runs.
That's correct, and concurrency: allow is why. Separate tickets are
independent and should be handled at the same time. Only switch to queue if
your final step writes somewhere that can't handle two at once.
Nobody approved and the run failed.
That's the four-hour expiry doing its job. Either lengthen it, add a second
approver so it isn't one person's inbox, or change on_timeout behavior to
route somewhere else.
Next
- Set proper limits before you scale this up: guardrails.
- Understand what you just built: loops and steps.
- Learn the approval mechanics: interactions.
- Fix one that broke: recover a failed run.
- Try another complete example: send a morning brief to Slack, triage GitHub issues, review every pull request, or run a daily security check.