# Mail triage

{/* Generated from backend/features/recipes/catalogue/mail_triage.yaml by `make arch`. Never edit this page: edit the recipe and run `make arch`, or `--check` fails. */}

Decide what each mail on one connection calls for -- answer, draft, internal only, escalate or ignore -- with a small model, and wake your agent only for the mail that needs it.

This page is made from the recipe `mail_triage`, version 1, and says nothing the recipe does not. [How a recipe is composed](/cookbook) explains every word it uses.

## Its setup

Each mail on \[The email connection Mail triage answers\] is read once: \[The agent that answers and drafts\] answers or drafts, \[The person escalations go to\] gets escalations, and \[Label for what is answered\], \[Label for what is drafted\], \[Label for what is internal only\], \[Label for what is escalated\] and \[Label for what is ignored\] mark outcomes.

Each phrase in brackets is a parameter the recipe asks for, named as the table of parameters below names it.

## Its parts, in the order data travels

1. **thread_flow `answers`** — Makes this triage the flow the connection answers each mail with.
   - answers the connection `${param.connection}` (the value of the parameter `connection`) with the flow `triage`, in place of the flow it answered with
2. **flow `triage`** — Decides what each mail calls for and does it, by the outcome the reading gives.
   - step `read` runs the tool `classify`
   - step `label_answer` runs the tool `put_label`, only while `label_answer` has a value
   - step `answered` runs the tool `answer`
   - step `label_draft` runs the tool `put_label`, only while `label_draft` has a value
   - step `drafted` runs the tool `draft`
   - step `label_internal` runs the tool `put_label`, only while `label_internal` has a value
   - step `kept_inside` runs the tool `declare`
   - step `label_escalate` runs the tool `put_label`, only while `label_escalate` has a value
   - step `escalated` runs the tool `to_people`
   - step `label_ignore` runs the tool `put_label`, only while `label_ignore` has a value
   - step `ignored` runs the tool `declare`
   - step `unclear` runs the tool `declare`
   - step `unclear_to_people` runs the tool `to_people`
3. **tool `classify`** — Asks once which of the five outcomes the mail calls for, and why.
   - runs the built-in `structured_answers`
4. **tool `put_label`** — Puts the label chosen for the outcome on the conversation.
   - runs the built-in `label_conversation`
5. **tool `declare`** — Notes the outcome on the decision record where there is nothing more to do.
   - runs the built-in `pass_value`
6. **tool `to_people`** — Leaves the thread with the person you chose, or waiting for a person, saying why.
   - runs the built-in `hand_to_person`
7. **tool `answer`** — Hands the mail to the agent you chose, which replies as the connection's mode allows.
   - runs the built-in `hand_to_agent`
8. **tool `draft`** — Hands the mail to the agent you chose, which prepares the reply for a person to send.
   - runs the built-in `hand_to_agent`

## Its parameters

| Parameter | Kind | What it asks | Value |
|---|---|---|---|
| `connection` | connection | The email connection Mail triage answers | required; fixed once installed |
| `agent` | agent | The agent that answers and drafts — It reads only the mail that is answered or drafted. | required; fixed once installed |
| `person` | person | The person escalations go to — Leave it blank and an escalated thread waits for a person. | may be left blank |
| `model` | text | The model that reads each mail — A model the platform offers. Blank, the platform's own reading model reads. | default `""` |
| `line_answer` | text | What is answered — One line; it is all the model reads of this outcome. | default `Routine mail the agent can answer on its own: a question its knowledge covers, a confirmation or a simple request.` |
| `line_draft` | text | What is drafted for review — One line; it is all the model reads of this outcome. | default `Mail the agent can answer but whose reply a person should read first: a commitment, a price, a complaint or anything delicate.` |
| `line_internal` | text | What is dealt with without replying — One line; it is all the model reads of this outcome. | default `Mail that calls for something on our side but no reply to the sender: a notice to file, a receipt, an alert.` |
| `line_escalate` | text | What goes to a person — One line; it is all the model reads of this outcome. | default `Mail only a person can handle: a legal threat, a large amount or anything the agent should not decide.` |
| `line_ignore` | text | What is ignored — One line; it is all the model reads of this outcome. | default `Mail nobody needs to act on: newsletters, advertising or automatic notices with nothing to do.` |
| `label_answer` | labels | Label for what is answered — Put on the conversation after the decision; blank, none. | may be left blank |
| `label_draft` | labels | Label for what is drafted — Put on the conversation after the decision; blank, none. | may be left blank |
| `label_internal` | labels | Label for what is internal only — Put on the conversation after the decision; blank, none. | may be left blank |
| `label_escalate` | labels | Label for what is escalated — Put on the conversation after the decision; blank, none. | may be left blank |
| `label_ignore` | labels | Label for what is ignored — Put on the conversation after the decision; blank, none. | may be left blank |

## How its data moves

The flow `triage`:

| Step | Input | Value | Comes from |
|---|---|---|---|
| `read` | `text` | `To: $ref:trigger.mail.to Cc: $ref:trigger.mail.cc $ref:trigger.text` | `mail.to` of what set the run off; `mail.cc` of what set the run off; `text` of what set the run off |
| `label_answer` | `add` | `${param.label_answer}` | replace with the value of the parameter `label_answer` |
| `answered` | `text` | `$ref:trigger.text` | `text` of what set the run off |
| `answered` | `messages` | `$ref:trigger.messages` | `messages` of what set the run off |
| `label_draft` | `add` | `${param.label_draft}` | replace with the value of the parameter `label_draft` |
| `drafted` | `text` | `$ref:trigger.text` | `text` of what set the run off |
| `drafted` | `messages` | `$ref:trigger.messages` | `messages` of what set the run off |
| `label_internal` | `add` | `${param.label_internal}` | replace with the value of the parameter `label_internal` |
| `kept_inside` | `value.outcome` | `internal_only` | fixed |
| `label_escalate` | `add` | `${param.label_escalate}` | replace with the value of the parameter `label_escalate` |
| `escalated` | `reason` | `Mail triage: $ref:read.reason` | `reason` of the step `read`'s answer |
| `label_ignore` | `add` | `${param.label_ignore}` | replace with the value of the parameter `label_ignore` |
| `ignored` | `value.outcome` | `ignore` | fixed |
| `unclear` | `value.outcome` | `escalate` | fixed |
| `unclear_to_people` | `reason` | `Mail triage could not tell what this mail calls for, so a person looks at it.` | fixed |

A value written `$ref:<step>.<path>` is what an earlier step answered, and `$ref:trigger.<path>` is what set the run off. A value with `${...}` in it is a placeholder: building it by hand, replace it with your own.

## Build it yourself

To install it as it is: `POST /api/v1/recipes/{key}/install` with the key `mail_triage` and a value for each parameter. To build the same thing with your own particulars, make each part in this order -- each one may name the ones before it -- with the operations below.

The YAML under each part is the recipe's declaration, not a request body: the API takes other names for the same things (a tool's `builtin` is `source` and `config.builtin`, a step's `tool` is `tool_id`, an edge's `from` is `source_step_id`). [From a recipe to the API](/cookbook#from-a-recipe-to-the-api) on the lead page maps every field, kind by kind.

### 1. tool `classify`

Asks once which of the five outcomes the mail calls for, and why.

- `POST /api/v1/tools` creates the instance: a built-in's, or a flow offered as one tool by its `config.flow_id`.

```yaml
kind: tool
key: classify
name: Mail triage on ${param.connection.name} — what the mail calls for
says: Asks once which of the five outcomes the mail calls for, and why.
builtin: structured_answers
config:
  model: ${param.model}
  fields:
  - key: outcome
    type: option
    options:
    - answer
    - draft
    - internal_only
    - escalate
    - ignore
    description: 'What this mail calls for, judging by its text, its subject, and whom it
      was addressed and copied to. Answer with exactly one of these:

      - answer: ${param.line_answer}

      - draft: ${param.line_draft}

      - internal_only: ${param.line_internal}

      - escalate: ${param.line_escalate}

      - ignore: ${param.line_ignore}'
  - key: reason
    type: string
    description: In one short sentence, why this mail calls for that outcome.
```

### 2. tool `put_label`

Puts the label chosen for the outcome on the conversation.

- `POST /api/v1/tools` creates the instance: a built-in's, or a flow offered as one tool by its `config.flow_id`.

```yaml
kind: tool
key: put_label
name: Mail triage on ${param.connection.name} — put the label on
says: Puts the label chosen for the outcome on the conversation.
builtin: label_conversation
```

### 3. tool `declare`

Notes the outcome on the decision record where there is nothing more to do.

- `POST /api/v1/tools` creates the instance: a built-in's, or a flow offered as one tool by its `config.flow_id`.

```yaml
kind: tool
key: declare
name: Mail triage on ${param.connection.name} — note the outcome
says: Notes the outcome on the decision record where there is nothing more to do.
description: The step that ends a branch with nothing to do, so the flow's record carries
  the outcome a person fixed on it. It sends and hands over nothing.
builtin: pass_value
```

### 4. tool `to_people`

Leaves the thread with the person you chose, or waiting for a person, saying why.

- `POST /api/v1/tools` creates the instance: a built-in's, or a flow offered as one tool by its `config.flow_id`.

```yaml
kind: tool
key: to_people
name: Mail triage on ${param.connection.name} — to a person
says: Leaves the thread with the person you chose, or waiting for a person, saying why.
builtin: hand_to_person
config:
  person_id: ${param.person}
```

### 5. tool `answer`

Hands the mail to the agent you chose, which replies as the connection's mode allows.

- `POST /api/v1/tools` creates the instance: a built-in's, or a flow offered as one tool by its `config.flow_id`.

```yaml
kind: tool
key: answer
name: Mail triage on ${param.connection.name} — answered by ${param.agent.name}
says: Hands the mail to the agent you chose, which replies as the connection's mode allows.
description: Hands what arrived on ${param.connection.name} to ${param.agent.name}, to reply.
builtin: hand_to_agent
config:
  agent_id: ${param.agent}
  mode: send
deadline_seconds: 120
```

### 6. tool `draft`

Hands the mail to the agent you chose, which prepares the reply for a person to send.

- `POST /api/v1/tools` creates the instance: a built-in's, or a flow offered as one tool by its `config.flow_id`.

```yaml
kind: tool
key: draft
name: Mail triage on ${param.connection.name} — drafted by ${param.agent.name}
says: Hands the mail to the agent you chose, which prepares the reply for a person to send.
description: Hands what arrived on ${param.connection.name} to ${param.agent.name}, to prepare
  a reply.
builtin: hand_to_agent
config:
  agent_id: ${param.agent}
  mode: prepare
deadline_seconds: 120
```

### 7. flow `triage`

Decides what each mail calls for and does it, by the outcome the reading gives.

- `POST /api/v1/flows` creates the flow.
- `POST /api/v1/flows/{flow_id}/steps` adds each step, naming the tool it runs.
- `POST /api/v1/flows/{flow_id}/edges` adds each edge.
- `PATCH /api/v1/flows/{flow_id}/entry-step` sets the step a run starts at.

```yaml
kind: flow
key: triage
name: Mail triage on ${param.connection.name}
says: Decides what each mail calls for and does it, by the outcome the reading gives.
steps:
- key: read
  tool: ${resource.classify}
  params:
    text: 'To: $ref:trigger.mail.to

      Cc: $ref:trigger.mail.cc


      $ref:trigger.text'
- key: label_answer
  tool: ${resource.put_label}
  params:
    add: ${param.label_answer}
  when: ${param.label_answer}
- key: answered
  tool: ${resource.answer}
  params:
    text: $ref:trigger.text
    messages: $ref:trigger.messages
- key: label_draft
  tool: ${resource.put_label}
  params:
    add: ${param.label_draft}
  when: ${param.label_draft}
- key: drafted
  tool: ${resource.draft}
  params:
    text: $ref:trigger.text
    messages: $ref:trigger.messages
- key: label_internal
  tool: ${resource.put_label}
  params:
    add: ${param.label_internal}
  when: ${param.label_internal}
- key: kept_inside
  tool: ${resource.declare}
  params:
    value:
      outcome: internal_only
- key: label_escalate
  tool: ${resource.put_label}
  params:
    add: ${param.label_escalate}
  when: ${param.label_escalate}
- key: escalated
  tool: ${resource.to_people}
  params:
    reason: 'Mail triage: $ref:read.reason'
- key: label_ignore
  tool: ${resource.put_label}
  params:
    add: ${param.label_ignore}
  when: ${param.label_ignore}
- key: ignored
  tool: ${resource.declare}
  params:
    value:
      outcome: ignore
- key: unclear
  tool: ${resource.declare}
  params:
    value:
      outcome: escalate
- key: unclear_to_people
  tool: ${resource.to_people}
  params:
    reason: Mail triage could not tell what this mail calls for, so a person looks at it.
edges:
- from: read
  to: label_answer
  condition:
    field: read.outcome
    op: ===
    value: answer
- from: read
  to: answered
  condition:
    field: read.outcome
    op: ===
    value: answer
- from: read
  to: label_draft
  condition:
    field: read.outcome
    op: ===
    value: draft
- from: read
  to: drafted
  condition:
    field: read.outcome
    op: ===
    value: draft
- from: read
  to: label_internal
  condition:
    field: read.outcome
    op: ===
    value: internal_only
- from: read
  to: kept_inside
  condition:
    field: read.outcome
    op: ===
    value: internal_only
- from: read
  to: label_escalate
  condition:
    field: read.outcome
    op: ===
    value: escalate
- from: read
  to: escalated
  condition:
    field: read.outcome
    op: ===
    value: escalate
- from: read
  to: label_ignore
  condition:
    field: read.outcome
    op: ===
    value: ignore
- from: read
  to: ignored
  condition:
    field: read.outcome
    op: ===
    value: ignore
- from: read
  to: unclear
- from: label_answer
  to: answered
- from: label_draft
  to: drafted
- from: label_internal
  to: kept_inside
- from: label_escalate
  to: escalated
- from: label_ignore
  to: ignored
- from: unclear
  to: unclear_to_people
```

### 8. thread_flow `answers`

Makes this triage the flow the connection answers each mail with.

No public operation places a flow on a connection that already answers. The nearest by hand makes the connection's channel with the flow, which a connection already answered has to be made again for:

- `POST /api/v1/channels` makes the connection's channel, answered by the flow it names.

```yaml
kind: thread_flow
key: answers
name: Mail triage answers ${param.connection.name}
says: Makes this triage the flow the connection answers each mail with.
connection: ${param.connection}
flow: ${resource.triage}
```

## How to adapt it

Choose the connection, the agent that answers and drafts, the person escalations go to -- or none, and the thread waits for a person -- and, if you want them, a label for each outcome and the smaller model that reads. Tune what goes under each outcome by rewriting its line, which is all the model reads of the recipe. To build your own, keep its shape: a reading step whose only answers are the five outcomes, a branch for each, the agent handed the mail in the mode the outcome calls for, and a label put on only where one was chosen.

## What cannot change once installed

Installed, every part is the recipe's: it is listed where its kind lives, marked as the recipe's, and refuses every edit and deletion by hand, naming the recipe.

- tool `classify`: its configuration and fixed values.
- tool `put_label`: its configuration and fixed values.
- tool `declare`: its configuration and fixed values.
- tool `to_people`: its configuration and fixed values.
- tool `answer`: its configuration and fixed values.
- tool `draft`: its configuration and fixed values.
- flow `triage`: its steps and edges.
- thread_flow `answers`: the connection it answers and the flow it runs there.

What stays the organization's:

- the values of `person`, `model`, `line_answer`, `line_draft`, `line_internal`, `line_escalate`, `line_ignore`, `label_answer`, `label_draft`, `label_internal`, `label_escalate`, `label_ignore`, changed on the installed recipe and written through to every part that uses them;
- not `connection`: the connection is fixed once installed, because it says which install this is -- answering another connection is installing the recipe there;
- not `agent`: the agent it names, or naming none, is fixed once installed -- answering the connection with another is uninstalling the recipe and installing it again;
- whether the recipe is on, which switches its automations on and off together;
- presentation -- a column's width, a step's position -- which changes nothing it runs.

## What it costs

What its parts consume -- a tool execution, a model's tokens, a message, storage -- is recorded and charged as those parts are, exactly as your own use of them is; nothing is charged for the recipe itself.
