Email triage: action catalogue¶
Status: draft. Not finalised, and the per-product versus universal question is still open.
The catalogue is the fixed menu of things the brain is allowed to propose. It returns a choice from this list, never free-form instructions:
n8n has a branch per action. Anything not on the list has no branch, so the worst an LLM can decide is the wrong action from a known set, never a novel one. Same reasoning as ADR-0004's structured predicates: no expression language, no eval, no improvisation.
Draft entries¶
| Action | Does | Approval |
|---|---|---|
draft_reply |
Writes a substantive draft. Does not send | Always |
draft_acknowledgement |
Writes an acknowledgement only. Does not send | Configurable |
create_job |
Raises a job in the client's system | Always |
close_item |
Ends the loop, nothing needed | Configurable |
Classification is not here. Classifying is an assessment, not something n8n executes. The catalogue holds only operations with an external effect (ADR-0032 ยง6d).
Escalation is not here either, and used to be. See below.
Escalation is a flag, not an action¶
An earlier version of this design had escalate_item as a catalogue action, which made escalation an alternative to drafting. That was wrong, and it made escalations less useful than they should be.
| Old | Now | |
|---|---|---|
| Shape | An action, instead of a draft | A flag on the work item |
| A flagged item | Went to a human with nothing | Goes to a human, often with a draft |
| The human's job | Read, understand, write | Decide, then approve, edit or discard |
Deciding is not the same as typing. A guardrail saying a human must handle a legal threat means a human must decide the response, not that a human must compose every sentence of it. The draft still requires approval, so nothing is weakened by providing one.
An escalation with no draft is a notification rather than a product: the human does all the work they would have done without us.
The flag¶
| Field | Holds |
|---|---|
attention_reason |
Nullable. Null means routine. Otherwise, why this one needs care |
attention_detail |
Free text for the human |
status records the outcome. attention_reason records why attention was needed. Two separate things, and conflating them was the original mistake.
Why this cannot live in status
Since flagging no longer replaces drafting, a flagged item can be sent by
us. An item flagged legal_risk may carry a draft, be approved, and go
out.
Under the old design that item's status would have been
escalated_by_design, which would be false: we sent it. The outcome is
sent_by_us, and the reason it needed care is a separate fact.
Two kinds of reason¶
The kind is a property of the value, not a stored field, the same way references_existing_work is a property of a category.
| Worked correctly | Fell short | |
|---|---|---|
| Did we understand the email? | Yes, completely | Not fully |
| Could we have acted? | Yes, technically | No |
| Why we did not | A rule said a human decides | We could not settle it |
| In value reporting | Handled | Not handled |
| The human needs | Why it was flagged | What we did work out, and what we could not |
Without the split, "we flagged 30%" could be competence or confusion, and those need opposite responses.
Every value¶
attention_reason |
Kind | Set at | Draft | Also alerts Console |
|---|---|---|---|---|
| null | Routine | Full | ||
legal_risk |
Worked correctly | Guardrail, 1.2.1 | Full | |
safety |
Worked correctly | Guardrail, 1.2.1 | Full | |
complaint |
Worked correctly | Guardrail, 1.2.1 | Full | |
high_value |
Worked correctly | Guardrail, 1.2.1 | Full | |
relationship |
Worked correctly | Guardrail, 1.2.1 | Full | |
new_customer |
Worked correctly | Guardrail, 1.2.1 | Full | |
never_automate |
Worked correctly | Guardrail, 1.2.1 | None. They asked for none | |
ambiguous_job |
Fell short | Resolve, 1.2.2 | Full. The uncertainty is the reply | |
multi_category |
Fell short | Decide, 1.2.3 | Full, addressing both | |
unclear_category |
Fell short | Classify, 1.2.1 | Acknowledgement only | |
unresolved_reference |
Fell short | Resolve, 1.2.2 | Acknowledgement only | |
parse_failure |
Fell short | Classify, 1.2.1 | None | Yes |
action_not_permitted |
Fell short | Decide, 1.2.3 | None. A rule said no | |
no_permitted_action |
Fell short | Decide, 1.2.3 | None | Yes |
action_not_enabled |
Fell short | Decide, 1.2.3 | None | Yes |
call_ceiling |
Fell short | Any brain call | None | Yes |
The "worked correctly" reasons come from rules, deterministically, not from the model's discretion (ADR-0005).
ambiguous_job is the case where the uncertainty is the reply. We know the customer and their open jobs, so asking which one is both the honest answer and the useful one.
never_automate is the exception on drafting. A client who said a category must always be human did not ask for a starting point, and providing one ignores what they said.
Four alert the Console because they mean something is broken rather than merely uncertain:
| Why we need telling | |
|---|---|
parse_failure |
A prompt or model regression |
no_permitted_action |
Rules contradict each other. Config is wrong |
action_not_enabled |
Rules point at something that does not exist |
call_ceiling |
Something is looping. Each one is a bug |
The rest are the system correctly reporting uncertainty. A rising rate is a quality signal worth acting on, but no single instance is a fault.
A pure system failure, meaning a provider outage or an unreachable database, goes to the Console only and never reaches the client (where a problem goes). The work simply stays unprocessed.
draft_acknowledgement¶
An acknowledgement, not an answer. "Thanks, we have your message, someone will come back to you today."
Why it may be the highest-value action in the product: response time is what clients complain about, and an acknowledgement needs no understanding of the request at all. It is always safe, it stops the customer waiting in silence, and it buys the human time to deal with the item properly.
It is derived, not chosen¶
The model always proposes draft_reply, because at 1.2.1 it cannot know whether Resolve will find enough to say anything substantive. Code downgrades at 1.2.3, once attention_reason is known.
attention_reason |
Draft |
|---|---|
| null, or a "worked correctly" reason | draft_reply |
ambiguous_job, multi_category |
draft_reply. The uncertainty is the reply |
unclear_category, unresolved_reference |
Downgrade to draft_acknowledgement |
parse_failure, no_permitted_action, action_not_permitted, action_not_enabled, call_ceiling |
No draft at all |
never_automate |
No draft. They asked for none |
The test is: do we know enough to say something useful? If we cannot tell what they are asking, an acknowledgement is all that is honest.
If acknowledgement_enabled is false, "downgrade" becomes "no draft". The human gets the item with nothing, which is what that client chose.
No model call¶
It is a template render, not a generation. model_calls does not increment.
That also makes it cheap enough to be worth having on by default for slow-responding clients, and it removes any question of the acknowledgement saying something it should not.
Why a separate action rather than a parameter¶
The content difference is thin, and both land in the same review queue. What justifies a catalogue entry is the machinery around it.
draft_reply |
draft_acknowledgement |
|
|---|---|---|
| Enabled per tenant | Always on | acknowledgement_enabled, off by default |
| Approval floor | Can never be relaxed | Could eventually send unsupervised |
| Model call | Yes | No |
| Says anything about the request | Yes | No, which is why it can be safe |
Enablement and approval floors both work at action granularity. As a parameter on draft_reply it would need parallel machinery to switch off and to treat approval differently, making it a special case rather than a catalogue entry.
In the queue¶
Low priority, regardless of the item's own priority. An acknowledgement is safe and says nothing, so it should not compete for attention with drafts that actually need reading.
Never alongside a substantive draft. One or the other.
Off by default. An acknowledgement from a firm that always answers within the hour looks like padding. From one where things sit for two days it is the biggest improvement available.
Notification urgency follows the reason¶
Per-tenant config maps reasons to urgency. "Look at this now" and "look at this when you can" are different asks, and that is where the distinction matters operationally rather than only in reporting.
Naming¶
verb_object, lowercase, snake_case, present tense.
| Good | Avoid | Why |
|---|---|---|
draft_reply |
replyDrafter |
Reads as an instruction, not a component |
create_job |
job_creation |
Verb first says what happens |
escalate_item |
escalate |
Bare verbs get ambiguous as the list grows |
close_item |
noAction |
Says what it does, not what it is not |
- The verb states the real effect, not the intent.
draft_replywrites a draft,send_replysends one. An approval log must distinguish those without anyone opening the code. - Never repurpose a name. A changed meaning becomes
draft_reply_v2, because existing approval records reference the old meaning and those records are evidence. - The object uses domain language.
create_job, neverpost_aroflo_workorder. The catalogue must survive replacing AroFlo with Simpro. - No product prefix in the name. Product is a field on the entry, so the taxonomy is data rather than string convention.
Parameters are typed¶
Every action declares a Pydantic model, versioned with the catalogue. Without it, params is a free-form dictionary and the closed catalogue leaks, because the model could put anything there and n8n would either ignore it or trust it.
Properties of the work item do not travel as action parameters. Priority is the clearest case: it belongs to the item, not to the act of drafting a reply. Carried on the action it could only be set while drafting, two actions could set it inconsistently, and closing an item could not set it at all.
Amending the catalogue¶
| Change | How |
|---|---|
| Add an action | Code. It needs an n8n branch. Define, implement, deploy |
| Give a tenant an action | Config. Enabled per tenant in the registry |
| Change an action's meaning | New name. Never repurpose |
| Remove an action | Deprecate slowly. In-flight items are pinned to their catalogue version |
The catalogue is versioned, and a work item stays pinned to the version in force when it started. Otherwise a required parameter added in version 4 breaks every item mid-flight under version 3.
Open¶
Is the catalogue per product or universal? Tracked as OI-067.
draft_reply is email triage. create_job is arguably not. If universal, a tenant's brain could propose actions their n8n does not implement. If per-product, something must resolve which catalogue applies.
Current lean: universal catalogue with per-tenant enablement in config, so it stays "config, not code" and the brain cannot propose an action the tenant has not switched on. Not yet decided.