Tag Vocabulary

Capabilities, priority, and tags describe tasks and agents in @nolag/agents. This page is the reference for each, and for which of them the SDK uses for routing (only capabilities) versus carries for your code to read (priority and tags).

Capabilities

A capability is a single string naming something a worker can do. Workers advertise a list of them through presence; a task names exactly one. dispatch() refuses to publish unless a connected agent advertises that capability, and onTask() hands a worker only the tasks whose capability is in its list.

import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, Handoff } from "@nolag/agents";

// Worker advertises capabilities through presence
const client = NoLag(WORKER_TOKEN);
const agents = new NoLagAgents({
  client,
  appName: APP_SLUG,
  agentId: "analyst-1",
  presence: { name: "analyst-1", role: "agent", capabilities: ["research", "finance", "summarization"] },
});
await client.connect();
await agents.ready();

const room = agents.room("default-workflow");
const handoff = new Handoff(room);

// ...and handles the ones it wants
handoff.onTask(["research", "finance"], (task, respond) => {
  respond("success", { handled: task.capability });
});

// Orchestrator dispatches for one capability
await handoff.dispatch("finance", { ticker: "ACME" });

// Who can do what right now, from presence
console.log(room.findAgents("finance").map((a) => a.name));
console.log(room.getAvailableCapabilities());

A task has one capability, so there is no "worker must have both" matching. If a job needs two skills, either name a combined capability (analyze-earnings) or dispatch two tasks.

Common capability names

CapabilityDescription
researchWeb research, data gathering, source verification
draftingContent creation, report writing, email composition
reviewQuality review, fact-checking, compliance checks
summarizationText summarization, key point extraction
financeFinancial analysis, calculations, reporting
codeCode generation, review, debugging
translationLanguage translation, localization
customer-supportUser queries, issue resolution, FAQ

These are conventions, not enforced values. Use any string that makes sense for your domain.

Priority

priority is a field on the task envelope. It has four values, defaults to medium, and is delivered to the worker as-is. The SDK does not queue, reorder, or preempt tasks by priority; a worker that cares reads task.priority and decides what to do.

import { Handoff } from "@nolag/agents";

const handoff = new Handoff(room);

// Dispatch with a priority
await handoff.dispatch("drafting", { report: "q3" }, { priority: "critical" });

// A worker reads it and chooses its own policy
handoff.onTask(["drafting"], async (task, respond) => {
  if (task.priority === "low" && busy()) {
    respond("error", {}, { code: "busy", message: "deferring low-priority work" });
    return;
  }
  respond("success", { draft: await draft(task.payload) });
});

Priority values

PrioritySuggested meaning
criticalDrop other work for this
highAhead of routine work
mediumDefault
lowBackground work

The meanings are suggestions for your worker code; the transport treats all four identically.

Task outcomes

There are no automatic status tags or lifecycle events. A task has two observable moments: the dispatcher publishes it, and the worker calls respond(). The result envelope carries the outcome:

result.statusMeaning
successWorker finished; payload holds the output
errorWorker reported a failure; error.code and error.message say why
partialWorker returned an incomplete result

A task nobody answers has no status at all: with waitForResult: true the dispatch promise rejects when the timeout passes. Anything in between (started, claimed, retrying) exists only if your worker emits it with Observe.

Custom tags

Tasks carry an optional tags: string[]. The convention is prefix:value, and the tag() helper builds one; TAG_PREFIX and TAG_FLAGS hold the standard prefixes and flags. Agents carry free-form metadata on their presence instead. Both are delivered as data for your code to read; neither is consulted by dispatch() or onTask().

import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, Handoff, tag, TAG_PREFIX, TAG_FLAGS } from "@nolag/agents";

// Tags on a task
const handoff = new Handoff(room);
await handoff.dispatch(
  "review",
  { documentId: "doc-42" },
  {
    tags: [
      tag(TAG_PREFIX.TENANT, "acme-corp"), // "tenant:acme-corp"
      tag("department", "legal"),          // "department:legal"
      tag("region", "eu-west"),
      TAG_FLAGS.REQUIRES_AUDIT,            // "requires_audit"
    ],
  },
);

// A worker reads them
handoff.onTask(["review"], (task, respond) => {
  const audited = task.tags?.includes(TAG_FLAGS.REQUIRES_AUDIT) ?? false;
  respond("success", { audited });
});

// Metadata on an agent, advertised through presence
const client = NoLag(REVIEWER_TOKEN);
const reviewer = new NoLagAgents({
  client,
  appName: APP_SLUG,
  agentId: "reviewer-eu-1",
  presence: {
    name: "reviewer-eu-1",
    role: "agent",
    capabilities: ["review"],
    metadata: { department: "legal", region: "eu-west", clearance: "confidential" },
  },
});

Custom tag patterns

PatternExampleUse case
Departmentdepartment:legalLet workers accept or decline by department
Region affinityregion:eu-westKeep data processing within a geographic region
Tenanttenant:acme-corpLabel work in multi-tenant agent systems (for hard isolation use Access Scopes)
Model selectionmodel:gpt-4oAsk for a specific LLM
Flagsrequires_human, requires_auditMark work that needs a human step or an audit entry

Server-side routing

By default every worker in a room receives every task and onTask discards the ones it cannot handle. For a busy room you can move that selection to the broker with subscription filters: a worker joins the room with the filter values it wants, and the dispatcher publishes each task tagged with one. Handoff.dispatch() publishes without a filter, so the dispatcher uses room.publishTask() directly and reads results from the room's result event.

import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, Handoff } from "@nolag/agents";

const client = NoLag(WORKER_TOKEN);
const agents = new NoLagAgents({
  client,
  appName: APP_SLUG,
  agentId: "researcher-1",
  presence: { name: "researcher-1", role: "agent", capabilities: ["research", "finance"] },
});
await client.connect();
await agents.ready();

// The broker now sends this worker only tasks published with one of these filter values
const room = agents.room("default-workflow", { filters: ["research", "finance"] });
new Handoff(room).onTask(["research", "finance"], (task, respond) => {
  respond("success", { done: task.taskId });
});

Two rules from the filters model apply here. A worker that subscribed without filters still receives filtered publishes, so unfiltered and filtered workers can coexist. With load balancing on, they should not: the broker treats the filtered and unfiltered subscriptions as separate share groups and a mixed pool delivers each task twice, so keep every worker in a group on the same filter shape.