---
title: Tag Vocabulary for Agents
description: Reference guide for capabilities, priority, and custom tags in @nolag/agents, and how task routing actually works.
---

# 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.

```typescript [TypeScript]
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

| Capability | Description |
| --- | --- |
| `research` | Web research, data gathering, source verification |
| `drafting` | Content creation, report writing, email composition |
| `review` | Quality review, fact-checking, compliance checks |
| `summarization` | Text summarization, key point extraction |
| `finance` | Financial analysis, calculations, reporting |
| `code` | Code generation, review, debugging |
| `translation` | Language translation, localization |
| `customer-support` | User 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.

```typescript [TypeScript]
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

| Priority | Suggested meaning |
| --- | --- |
| `critical` | Drop other work for this |
| `high` | Ahead of routine work |
| `medium` | Default |
| `low` | Background 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.status` | Meaning |
| --- | --- |
| `success` | Worker finished; `payload` holds the output |
| `error` | Worker reported a failure; `error.code` and `error.message` say why |
| `partial` | Worker 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](/docs/agents/patterns#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()`.

```typescript [TypeScript]
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

| Pattern | Example | Use case |
| --- | --- | --- |
| Department | `department:legal` | Let workers accept or decline by department |
| Region affinity | `region:eu-west` | Keep data processing within a geographic region |
| Tenant | `tenant:acme-corp` | Label work in multi-tenant agent systems (for hard isolation use [Access Scopes](/docs/scopes/getting-started)) |
| Model selection | `model:gpt-4o` | Ask for a specific LLM |
| Flags | `requires_human`, `requires_audit` | Mark 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.

```typescript [Worker]
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 });
});
```
```typescript [Dispatcher]
import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, createTaskEnvelope } from "@nolag/agents";

const client = NoLag(ORCHESTRATOR_TOKEN);
const agents = new NoLagAgents({ client, appName: APP_SLUG, agentId: "orchestrator" });
await client.connect();
await agents.ready();

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

// Results come back to this agent's id; read them from the room
room.on("result", (result) => {
  console.log(result.taskId, result.status, result.payload);
});

// Publish with the capability as the filter value
const task = createTaskEnvelope("research", { url: "https://example.com" }, { priority: "high" });
room.publishTask(task, { filter: task.capability });
```

Two rules from the [filters](/docs/concepts/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.
