Skip to content
NewCLI, MCP, HTTP and MQTT on one boundary

Your workflow owns causality.Long Tail owns uncertainty.

The execution environment for everything your workflow waits on but doesn't control. A person, an agent, a script or a machine resolves the wait, and your code picks up exactly where it paused.

  • Postgres is the only dependency
  • MCP native
  • Embeds in Node and NestJS

The boundary

Every real process reaches a point your code can't settle on its own.

Durable workflows are excellent at causality: run this activity, then that one, retry, wait for a condition. Then reality steps in. The workflow can say what it needs. How reality satisfies that need is out of its hands.

A person must inspect something

A reviewer checks the work before it ships, with a form that validates their answer.

A printer must become available

The order waits for capacity, then claims a free machine atomically.

Twenty physical items must arrive

One row fills item by item and wakes the workflow when the container is complete.

An AI result needs review

Confident answers pass straight through. The rest wait on a person or a second agent.

A machine lifecycle must finish

The machine reports over MQTT and resolves the same row a person would.

Another system must respond

A partner API, a lab, a carrier. Whoever answers settles the row by a key they know.

Without a boundary

Teams rebuild the same scaffolding around every wait.

Task tables, status columns, queues, claims, timeouts, forms, RBAC, callbacks, indexes, audit trails and the dashboards to watch them. Long Tail makes the boundary first-class, so all of it comes with one call.

Read why it exists
  • task tables
  • status columns
  • queues
  • assignments
  • claims
  • timeouts
  • forms
  • RBAC
  • callbacks
  • metadata indexes
  • batch operations
  • audit trails
  • dashboards
  • reporting
await conditional(signalId, { role, metadata, timeout });

The contract

Intent in. Resolution out.

The workflow provides an intent. Long Tail turns it into a searchable, claimable escalation and manages everything needed to resolve it. When reality produces an answer, Long Tail returns the resolution and durable execution continues.

  1. 01 · intent

    The workflow says what it needs

    A role, a deadline and the facts to route on. One crash-safe write.

  2. 02 · escalation

    Long Tail owns the wait

    Searchable, claimable, deadlined and role-gated. Every surface sees the same row.

  3. 03 · resolution

    The answer comes back typed

    A payload, false on timeout, null on cancel. Execution continues.

workflows/review.ts
ts
import { Durable } from '@hotmeshio/hotmesh';
import { conditional, type LTEnvelope } from '@hotmeshio/long-tail';
import * as activities from './activities';

const { analyzeContent } = Durable.workflow.proxyActivities<typeof activities>({ activities });

export async function reviewContent(envelope: LTEnvelope) {
  const analysis = await analyzeContent(envelope.data.content);
  if (analysis.confidence >= 0.85) {
    return { data: { approved: true, analysis } };
  }

  const { workflowId } = Durable.workflow.workflowInfo();
  const decision = await conditional<{ decision: 'approve' | 'reject'; notes?: string }>(`review-${workflowId}`, {
    role: 'reviewer',
    type: 'content-review',
    priority: 2,
    description: `Confidence ${analysis.confidence}. Needs a human.`,
    metadata: { contentId: envelope.data.contentId },
    timeout: '72h',
    schemaVersion: 1,
  });

  if (decision === false) return { data: { approved: false, reason: 'sla' } };
  if (decision === null) return { data: { cancelled: true } };
  return { data: { approved: decision.decision === 'approve', analysis } };
}
roles/reviewer · form schema v1
{
  "title": "Content Review",
  "required": ["decision"],
  "properties": {
    "decision": {
      "type": "string",
      "enum": ["approve", "reject"],
      "description": "Approve this content?"
    },
    "notes": {
      "type": "string",
      "format": "textarea",
      "description": "What you saw"
    }
  }
}

Renders as

reviewer · content-review

Content Review

claimed · 28m left

Confidence 0.62. Needs a human.

Decision *

Approve this content?

Notes

What you saw

Image matches the caption.
ReleaseSubmit

One row, every surface

CLI, MCP, HTTP and MQTT meet at one boundary.

A person resolves it on the dashboard, a service over HTTP, an operator from the terminal, an agent over MCP, and a machine over MQTT. The row, the wait and the result are the same in every case. So are the validation, the RBAC and the audit.

See every surface
workflows/review.ts
const decision = await conditional<{ decision: 'approve' | 'reject' }>(`review-${workflowId}`, {
  role: 'reviewer',
  metadata: { orderId: 'A-1042' },
  timeout: '72h',
});
// decision: the payload, false on timeout, null on cancel

The workflow states its intent and pauses. One crash-safe write mints the row, its facets and its deadline.

  • Dashboard

    Person

  • HTTP

    Service

  • SDK

    Service

  • CLI

    Operator

  • MCP

    Agent

  • MQTT

    Machine

The dashboard comes with it

Every role gets a workspace on day one.

Queues, rendered forms, claim timers, pinned views and wall-screen portals, all scoped by role. The role’s schema is the UI.

A claimed escalation in the Long Tail dashboard: a customer intake form with instructions and a claim timer
A reviewer portal with count tiles and pinned lists
Portals: counts and pinned lists for a station.
The reviewer queue as a filterable table
Queues: every row a role can see, filterable by facet.

What the primitive buys

The waiting queue gets qualities the deterministic one never needed.

RBAC is intrinsic

The queue is a role. Any member answers, a person in the dashboard or a service account through the API, and the workflow is indifferent to which.

The form is data

A versioned JSON Schema on the role renders the resolver UI and validates every submission on every surface.

Claims are locks

A claim is a time-boxed window with a resolve guard behind it. A submission against an expired claim is rejected in the same statement.

Cancel is home

Cancellation is a first-class settlement. The waiting flow sees null and reconciles to a modeled resting state.

One shared surface

Every actor writes to the same metadata-keyed row. One place to search, one place to claim, one place to audit.

History accretes

Intent at creation, outcome at settlement, every crossing in between. The row is the production log.

Objects get lifecycles

Give an object this queue and a loop, and it lives: it advertises when free, asks when it needs service and reconciles when reality answers.

What repeats stops needing people

Every settled row is a worked example. Recurring cases become deterministic tools.

Pattern library

A recipe for every kind of wait.

Timeouts that deliver, containers that fill, claims by attribute, badges that attribute without granting, machines that answer like people. Each pattern is a short page with the code and the guarantee.

Runs where you run

Point at Postgres. Everything else is optional.

Standalone with the dashboard, workers on their own, or embedded inside the app you already have. All modes share one database and scale independently.

index.ts
import { start } from '@hotmeshio/long-tail';
import { reviewContent } from './workflows/review';

await start({
  database: { connectionString: process.env.DATABASE_URL },
  workers: [{ taskQueue: 'default', workflow: reviewContent }],
});

Dashboard, API and workers in one process. The dashboard serves on port 3000.

The long tail gets shorter

What a person resolves today, a tool runs next month.

Every settled row records the intent, the answer and who gave it. Recurring cases move from people to agents to compiled, deterministic tools. The head of reliable, automated work grows. The tail of novel problems shrinks.

Who resolves a recurring case

Week 1
Month 1
Month 3
Month 6
  • Resolved by a person
  • Resolved by an agent
  • Resolved by a compiled tool

Illustrative. Each settled row is a worked example the next run can reuse.

Start with one await.

Install the package, point it at Postgres, and park your first intent. The dashboard is running before your coffee is.