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
| Topic | Purpose | QoS |
|---|---|---|
_handoff/dispatch | Task dispatch from orchestrator | 2 (exactly once) |
_handoff/result | Results from workers | 2 (exactly once) |
_handoff/claim | Worker claims a task | 1 (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
| Topic | Purpose | QoS |
|---|---|---|
_inbox/{actorId} | Messages for a specific agent | 2 (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
| Topic | Purpose | QoS |
|---|---|---|
_blackboard/state | State updates (retained) | 2 (exactly once) |
_blackboard/change | Change notifications | 1 (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
| Topic | Purpose | QoS |
|---|---|---|
_observe/events | All system events | 1 (at least once) |
_observe/custom | Custom application events | 0 (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
| Topic | Purpose | QoS |
|---|---|---|
_approve/request | Approval requests | 2 (exactly once) |
_approve/response | Approval decisions | 2 (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
| Topic | Purpose | QoS |
|---|---|---|
_tools/registry | Tool registration (retained) | 2 (exactly once) |
_tools/invoke | Tool invocation requests | 2 (exactly once) |
_tools/result | Tool invocation results | 2 (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.