Triage and resolution strategies
Some escalations need a fix, not a judgment: content in the wrong language, an image upside down, a record that needs data from another system. The resolver knows what is wrong but cannot produce the corrected data. Let them flag the row with a hint and hand the fix to tools and an AI agent, then continue the workflow with the corrected result.
There are two ways to build this loop, one per escalation style.
| Escalation style | Who decides what a resolution does | Where triage happens |
|---|---|---|
conditional (resumes in place) |
Your workflow code, after the wait returns | An activity or child workflow you call |
Re-run path ({ type: 'escalation' } returns) |
A pluggable resolution strategy | The built-in mcpTriage workflow, or your own |
With conditional: branch in your workflow
Section titled “With conditional: branch in your workflow”A conditional row resolves straight into the waiting workflow, so the triage branch is ordinary code. Add the flag and the hint to the role’s form, then act on them after the wait.
"needsRemediation": { "type": "boolean", "title": "Needs a fix before review" },"hint": { "type": "string", "title": "What is wrong", "x-lt-showIf": "resolver.needsRemediation", "x-lt-options": [ { "value": "wrong_language", "label": "Wrong language" }, { "value": "unreadable", "label": "Unreadable" } ]}type ReviewV2 = { approved?: boolean; needsRemediation?: boolean; hint?: 'wrong_language' | 'unreadable' };
let content = envelope.data.content;for (let round = 1; round <= 3; round++) { const decision = await conditional<ReviewV2>(`review-${workflowId}-${round}`, { role: 'reviewer', metadata: { contentId, round }, envelope: { content }, schemaVersion: 2, }); if (!decision) return { data: { contentId, approved: false } }; if (!decision.needsRemediation) return { data: { contentId, approved: !!decision.approved } };
content = await remediate({ content, hint: decision.hint }); // your activity: a tool, a model, a service}return { data: { contentId, approved: false, reason: 'unresolved after remediation' } };Each round parks a new row with its own signal id, so the history of every attempt stays on its own row. The remediation is a checkpointed activity like any other.
On the re-run path: resolution strategies
Section titled “On the re-run path: resolution strategies”On the re-run path, resolving a row starts the workflow again. Before it does, the resolve route asks the registered strategy what to do with the resolution:
resolver payload ──► strategy.onResolution(context) ├─ { action: 'rerun' } → start the workflow again with envelope.resolver └─ { action: 'triage' } → start a triage workflow with a triage envelopeThe strategy sees the escalation record, the resolver payload and the original envelope.
| Strategy | Behavior |
|---|---|
| Default | Always re-runs. Used when no strategy is set, or with strategy: 'default' |
| MCP | Reads resolverPayload._lt.needsTriage. When set, routes to mcpTriage. Otherwise re-runs |
| Custom | Your LTEscalationStrategy adapter decides |
await start({ database: { connectionString: process.env.DATABASE_URL }, workers: [/* ... */], escalation: { strategy: 'mcp' },});The _lt namespace
Section titled “The _lt namespace”The _lt key in a resolver payload is reserved for Long Tail control flow. Everything outside it is the resolver’s own data.
| Field | Type | Meaning |
|---|---|---|
_lt.needsTriage |
boolean |
Route to triage instead of a re-run |
_lt.hint |
string |
A remediation hint for the triage agent, such as 'wrong_language' |
{ "resolverPayload": { "_lt": { "needsTriage": true, "hint": "wrong_language" }, "notes": "The text is in German; the pipeline expects English" }}What mcpTriage does
Section titled “What mcpTriage does”When the MCP strategy routes a resolution to triage, the row is marked resolved and mcpTriageRouter starts. The router runs a compiled pipeline when one matches the problem, and otherwise hands off to mcpTriage:
- It reads the upstream runs and escalation history that share the row’s origin, to see what happened before.
- It loads the MCP tools scoped by the original workflow’s
toolTags, plus theworkflows,compiledanddatabasetags. - An LLM loop calls those tools with the hint as guidance, for example
translate_contentfrom the built-in translation server. - It re-invokes the original workflow with the corrected data, which completes and signals its parent through the usual channels. When the agent cannot fix the problem, it escalates to an engineer, and their answer re-enters the loop with any new tools they installed.
The triage workflows (mcpTriageRouter, mcpTriageDeterministic, mcpTriage) register on the long-tail-system queue when ANTHROPIC_API_KEY or OPENAI_API_KEY is set.
A triage run that works can be compiled into a deterministic pipeline. The next time the same problem appears, the router finds the compiled pipeline and runs it without an LLM.
Bulk triage
Section titled “Bulk triage”An admin can send several re-run rows to triage at once:
curl -X POST http://localhost:3000/api/escalations/bulk-triage \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{ "ids": ["<escalation-id>"], "hint": "wrong_language" }'# { "triaged": 1, "workflows": ["triage-..."] }Call bulk_triage with ids and an optional hint.
Write a custom strategy
Section titled “Write a custom strategy”Implement LTEscalationStrategy and return rerun or triage. A triage directive starts mcpTriageRouter with the envelope you return, so the simplest custom strategy decides when to triage and lets McpEscalationStrategy build that envelope:
import { McpEscalationStrategy, type LTEscalationStrategy, type ResolutionContext, type ResolutionDirective,} from '@hotmeshio/long-tail';
const mcp = new McpEscalationStrategy();
export class OutcomeStrategy implements LTEscalationStrategy { async onResolution(context: ResolutionContext): Promise<ResolutionDirective> { const { resolverPayload } = context; if (resolverPayload.outcome === 'unreadable') { return mcp.onResolution({ ...context, resolverPayload: { ...resolverPayload, _lt: { needsTriage: true, hint: 'unreadable' } }, }); } return { action: 'rerun' }; }}Register it at startup, or through the registry:
await start({ database, workers, escalation: { adapter: new OutcomeStrategy() } });import { escalationStrategyRegistry, McpEscalationStrategy } from '@hotmeshio/long-tail';
escalationStrategyRegistry.register(new McpEscalationStrategy());