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

  1. Open Build > Agents and click Create agent.
  2. Choose Custom agent and name it Triage.
  3. Set Agent description to Reads inbound tickets, classifies them, and drafts holding replies.
  4. 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.

  1. Click Create agent.

2. Give Triage an email address

  1. On the agent's page, find the Messaging section.
  2. Provision an inbox and copy the address it generates.
  3. 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

  1. Open Build > Loops and click New loop.
  2. Name it Triage tickets.
  3. Click Add trigger and choose Event.
  4. Set the event to email.received.
  5. 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.

  1. Name the step Classify ticket.
  2. Click Select agent and choose Triage.
  3. 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

  1. Add a step and choose Interaction.
  2. Name it Approve the reply.
  3. Set the request type to Approval.
  4. Set the responder to your service desk lead.
  5. 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.
  1. 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

  1. Add a step and choose Action.
  2. Name it Post to service desk.
  3. Choose the slack.message.post action.
  4. Set the channel to #service-desk.
  5. 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 }}
  1. 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.suspended

It 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.completed

Check #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