Skip to content

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:

{ "action": "draft_reply", "params": { "tone": "apologetic" }, "requires_approval": true }

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_reply writes a draft, send_reply sends 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, never post_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.