Coordination Patterns

The @nolag/agents SDK provides six coordination patterns. Each pattern maps to specific topics and QoS settings under the hood, but you interact with them through high-level APIs.

Handoff

The Handoff pattern implements task dispatch and result collection. An orchestrator dispatches tasks tagged with required capabilities. Workers register their capabilities and receive only tasks they can handle. Results flow back to the orchestrator automatically.

When to use

  • Distributing work across a pool of specialized agents
  • Load balancing tasks based on agent capabilities
  • Fan-out/fan-in workflows where multiple agents process subtasks

Dispatching tasks

// Orchestrator dispatches a task
const task = await room.handoff.dispatch({
  type: "summarize",
  payload: { url: "https://example.com/article" },
  capabilities: ["research"],       // Only workers with this tag receive it
  priority: "urgent",               // Optional priority tag
  timeout: 30000,                   // Optional timeout in ms
});

// Listen for the result
room.handoff.onResult((result) => {
  if (result.taskId === task.id) {
    console.log("Summary:", result.data.summary);
  }
});

// Handle timeout
room.handoff.onTimeout((taskId) => {
  console.log("Task timed out:", taskId);
});

Handling tasks

// Worker registers capabilities
room.handoff.register({
  capabilities: ["research", "finance"],
  concurrency: 3,  // Handle up to 3 tasks at once
});

// Handle incoming tasks
room.handoff.onTask(async (task) => {
  console.log("Received:", task.type, task.payload);

  try {
    const result = await doResearch(task.payload);
    await task.complete({ summary: result });
  } catch (err) {
    await task.fail({ error: err.message });
  }
});

Topic mapping

TopicPurposeQoS
_handoff/dispatchTask dispatch from orchestrator2 (exactly once)
_handoff/resultResults from workers2 (exactly once)
_handoff/claimWorker claims a task1 (at least once)

Inbox

The Inbox pattern provides direct actor-to-actor messaging. Unlike Handoff which broadcasts to capable workers, Inbox sends messages to a specific agent by ID. Use it for point-to-point communication between known agents.

When to use

  • Sending instructions to a specific agent
  • Agent-to-agent conversation threads
  • Passing results between pipeline stages with known recipients

Code example

// Send a direct message to a specific agent
await room.inbox.send("reviewer-agent", {
  type: "review-request",
  document: draftContent,
  deadline: "2024-01-15",
});

// Receive direct messages
room.inbox.on("message", (msg) => {
  console.log("From:", msg.sender);
  console.log("Type:", msg.data.type);
  console.log("Content:", msg.data);
});

Topic mapping

TopicPurposeQoS
_inbox/{actorId}Messages for a specific agent2 (exactly once)

Blackboard

The Blackboard pattern provides shared key-value state with versioning. All agents in a room see the same state. Updates are broadcast in real time so every agent stays synchronized. Versioning prevents conflicts from concurrent updates.

When to use

  • Sharing workflow state across multiple agents
  • Maintaining a shared plan or configuration
  • Accumulating results from multiple agents into a single view

Code example

// Write shared state
await room.blackboard.set("workflow-plan", {
  steps: ["research", "draft", "review", "publish"],
  currentStep: 0,
  assignees: {},
});

// Read shared state
const plan = room.blackboard.get("workflow-plan");
console.log("Current step:", plan.steps[plan.currentStep]);

// Update atomically
await room.blackboard.update("workflow-plan", (prev) => ({
  ...prev,
  currentStep: prev.currentStep + 1,
}));

// Watch for changes
room.blackboard.on("change", (key, value, version) => {
  console.log(`${key} updated to version ${version}`);
});

Topic mapping

TopicPurposeQoS
_blackboard/stateState updates (retained)2 (exactly once)
_blackboard/changeChange notifications1 (at least once)

Observe

The Observe pattern provides a real-time event stream for monitoring. Every action in the system (task dispatch, completion, state changes, approvals) emits an event. Subscribe to all events or filter by type for dashboards, logging, and debugging.

When to use

  • Building monitoring dashboards for agent workflows
  • Audit logging of all agent decisions
  • Debugging agent behavior in real time
  • Triggering side effects on specific events

Code example

// Watch all events
room.observe.on("*", (event) => {
  console.log(`[${event.timestamp}] ${event.agent} -> ${event.type}`);
});

// Watch specific event types
room.observe.on("task:dispatched", (event) => {
  console.log("New task:", event.data.taskId);
});

room.observe.on("task:completed", (event) => {
  console.log("Done:", event.data.taskId, event.data.result);
});

// Emit custom events
await room.observe.emit("custom:checkpoint", {
  step: "research-complete",
  duration: 4200,
});

Topic mapping

TopicPurposeQoS
_observe/eventsAll system events1 (at least once)
_observe/customCustom application events0 (fire and forget)

Approve

The Approve pattern implements human-in-the-loop gating. An agent can pause its workflow and request approval from a human (or supervisor agent) before proceeding. The request includes context about the pending action so the approver can make an informed decision.

When to use

  • Gating destructive or high-stakes actions
  • Compliance workflows requiring human sign-off
  • Multi-level approval chains
  • Allowing humans to modify agent outputs before execution

Code example

// Agent requests approval
const approval = await room.approve.request({
  action: "send-email",
  description: "Send quarterly report to investors",
  payload: { to: "investors@company.com", body: reportHtml },
  timeout: 600000,  // 10 minutes to respond
});

if (approval.approved) {
  await sendEmail(approval.payload);
} else {
  console.log("Rejected:", approval.reason);
}

// Human (or supervisor agent) handles approval requests
room.approve.onRequest((request) => {
  // Show in UI or evaluate programmatically
  displayApprovalDialog(request);
});

// Approve or reject
await room.approve.respond(request.id, {
  approved: true,
  // Or: approved: false, reason: "Budget not approved"
});

Topic mapping

TopicPurposeQoS
_approve/requestApproval requests2 (exactly once)
_approve/responseApproval decisions2 (exactly once)

Tools

The Tools pattern enables remote tool invocation. One agent can register tools that other agents can discover and call. This is similar to MCP tool use but runs over the NoLag messaging layer for real-time, multi-agent scenarios.

When to use

  • Sharing capabilities across agents (search, database access, APIs)
  • Building tool-augmented agents that compose external services
  • Centralizing tool access with access control

Code example

// Register a tool that other agents can invoke
room.tools.register("web-search", {
  description: "Search the web for information",
  parameters: { query: "string" },
  handler: async (params) => {
    const results = await searchWeb(params.query);
    return { results };
  },
});

// Invoke a tool from another agent
const result = await room.tools.invoke("web-search", {
  query: "latest quarterly earnings ACME Corp",
});

console.log("Search results:", result.results);

// List available tools
const tools = room.tools.list();
console.log("Available:", tools.map(t => t.name));

Topic mapping

TopicPurposeQoS
_tools/registryTool registration (retained)2 (exactly once)
_tools/invokeTool invocation requests2 (exactly once)
_tools/resultTool invocation results2 (exactly once)

Tenant Isolation

All six coordination patterns work seamlessly with Access Scopes for multi-tenant deployments. When you assign an agent actor to a scope, the coordination topics are automatically namespaced under the scope slug.

For example, a Handoff dispatch topic for a scoped actor becomes agents-app/tenant-slug/_handoff/dispatch instead of agents-app/_handoff/dispatch. This means an orchestrator in one tenant cannot dispatch tasks to workers in another tenant. The isolation applies to all patterns - Inbox, Blackboard, Observe, Approve, and Tools - with no changes to your agent code.

To set up tenant isolation for your agents, create a scope for each tenant and assign each tenant's agent actors to that scope. See the Multi-Tenant Patterns guide for step-by-step implementation details.