---
title: Getting Started with AI Agents
description: Step-by-step guide to building your first multi-agent workflow with @nolag/agents.
---

# Getting Started with AI Agents

Build your first multi-agent workflow in minutes.

## Prerequisites

- A NoLag account ([free tier available](/pricing))
- A project in the [portal](https://portal.nolag.app) and a project API key (`nlg_live_...`) for the one-time setup step
- Node.js 18+ (JavaScript) or Python 3.10+ (Python)

## Step 1: Install

```bash [npm]
npm install @nolag/agents @nolag/js-sdk
```
```bash [pip]
pip install nolag-agents
```

The agents SDK is available for **JavaScript/TypeScript** (`@nolag/agents`) and **Python** (`nolag-agents`). Both provide the same six coordination patterns.

## Step 2: Create an Agents App

**Before you start.** Create an app from the `nolag-agents-sdk` blueprint. That seeds the room this SDK expects (`default-workflow`, with the seven coordination topics) and the `agent-activity` lobby, and returns an app slug with a random suffix. That slug is the `appName` you pass to the wrapper. You can also do this in the portal: Apps, New App, pick the blueprint.

```typescript [Setup (server side, once)]
import { NoLagApi } from "@nolag/js-sdk";

const api = new NoLagApi(process.env.NOLAG_API_KEY); // nlg_live_...
const app = await api.apps.create({ name: "My Agents", blueprintId: "nolag-agents-sdk" });
console.log(app.slug); // e.g. "my-agents-a3f9": this is your appName

const orchestrator = await api.actors.create({ name: "orchestrator", actorType: "orchestrator" });
const worker = await api.actors.create({ name: "worker-1", actorType: "agent" });
console.log(orchestrator.accessToken, worker.accessToken); // shown once; keep them
```

Rooms never exist implicitly. `default-workflow` is seeded by the blueprint; any other room you want agents to join has to be created first, in the portal or with `api.rooms.create(app.appId, { name: "..." })`.

## Step 3: Create Actor Tokens

Each agent in your system connects as an **actor**. The setup block above creates two; you can also create them in the portal under Actors:

1. **Orchestrator** (`actorType: "orchestrator"`) dispatches tasks and coordinates the workflow
2. **Worker** (`actorType: "agent"`) receives and completes tasks

Only the `agent` and `orchestrator` actor types hold a persistent session on the broker. Copy the access token for each actor (`at_live_...`); it is shown once.

## Step 4: Connect the Orchestrator

The orchestrator dispatches tasks and waits for results. Your app owns the core client; the wrapper attaches to it.

```typescript [TypeScript]
import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, Handoff } from "@nolag/agents";

const client = NoLag(ORCHESTRATOR_TOKEN);
const agents = new NoLagAgents({
  client,
  appName: APP_SLUG, // the slug returned when you created the app
  agentId: "orchestrator",
  presence: { name: "orchestrator", role: "orchestrator" },
});
await client.connect();
await agents.ready();

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

// Dispatch a task to any connected worker advertising the "summarize" capability.
// dispatch() throws if no such worker is connected, and rejects on timeout.
const result = await handoff.dispatch(
  "summarize",
  { url: "https://example.com/article" },
  { waitForResult: true, timeout: 30000 },
);

if (result) {
  console.log("Result:", result.status, result.payload);
}
```
```python [Python]
from nolag_agents import NoLagAgents, NoLagAgentsOptions, AgentPresenceData
from nolag_agents.patterns import Handoff

agents = NoLagAgents(ORCHESTRATOR_TOKEN, NoLagAgentsOptions(
    app_name=APP_SLUG,  # the slug returned when you created the app
    agent_id="orchestrator",
    presence=AgentPresenceData(name="orchestrator", role="orchestrator"),
))
await agents.connect()

room = await agents.room("default-workflow")
handoff = Handoff(room)

# Dispatch a task to any connected worker advertising the "summarize" capability.
# dispatch() raises if no such worker is connected, and on timeout.
result = await handoff.dispatch("summarize",
    {"url": "https://example.com/article"},
    wait_for_result=True, timeout=30000,
)

print("Result:", result.status, result.payload)
```

`dispatch()` looks at room presence first: if no connected agent lists the capability, it throws instead of publishing. Start the worker before the orchestrator, or pass `allowNoWorkers: true` (`allow_no_workers=True`) to publish anyway.

## Step 5: Connect a Worker

Workers advertise their capabilities through presence and handle incoming tasks. Turn on load balancing on the client when you run more than one worker with the same capabilities: the `tasks` and `tools` topics then deliver each message to one member of the group instead of every member.

```typescript [TypeScript]
import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, Handoff } from "@nolag/agents";

// loadBalance: each task goes to one worker in the "summarizers" group
const client = NoLag(WORKER_TOKEN, { loadBalance: true, loadBalanceGroup: "summarizers" });
const agents = new NoLagAgents({
  client,
  appName: APP_SLUG,
  agentId: "worker-1",
  presence: { name: "worker-1", role: "agent", capabilities: ["summarize"] },
});
await client.connect();
await agents.ready();

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

// Handle incoming tasks whose capability matches
handoff.onTask(["summarize"], async (task, respond) => {
  console.log("Received task:", task.taskId, task.capability);

  const summary = await summarizeUrl(task.payload.url as string);
  respond("success", { summary });
});
```
```python [Python]
import asyncio

from nolag_agents import NoLagAgents, NoLagAgentsOptions, AgentPresenceData
from nolag_agents.patterns import Handoff

agents = NoLagAgents(WORKER_TOKEN, NoLagAgentsOptions(
    app_name=APP_SLUG,
    agent_id="worker-1",
    load_balance=True,  # each task goes to one worker in the group
    load_balance_group="summarizers",
    presence=AgentPresenceData(name="worker-1", role="agent", capabilities=["summarize"]),
))
await agents.connect()

room = await agents.room("default-workflow")
handoff = Handoff(room)

# Handle incoming tasks whose capability matches
def handle_task(task, respond):
    print("Received task:", task.task_id, task.capability)
    summary = summarize_url(task.payload["url"])
    asyncio.ensure_future(respond("success", {"summary": summary}))

handoff.on_task(["summarize"], handle_task)
```

Every worker in the room receives every task on `tasks`; `onTask` drops the ones whose `capability` it does not handle. To have the broker do that filtering instead, see [server-side routing](/docs/agents/tags#server-side-routing).

## Step 6: Monitor with Observe

Observe is an event stream that carries only what agents emit. Nothing is emitted automatically, so decide what you want to see and emit it from the worker. A monitor then subscribes to the stream:

```typescript [TypeScript]
import { NoLag } from "@nolag/js-sdk";
import { NoLagAgents, Handoff, Observe } from "@nolag/agents";

// In the worker: emit events at the points you want to see
const workerRoom = agents.room("default-workflow");
const workerObserve = new Observe(workerRoom, agents.agentId);
new Handoff(workerRoom).onTask(["summarize"], async (task, respond) => {
  workerObserve.emit("task:started", { taskId: task.taskId, capability: task.capability });
  const summary = await summarizeUrl(task.payload.url as string);
  respond("success", { summary });
  workerObserve.emit("task:completed", { taskId: task.taskId });
});

// In a monitor process: watch the stream
const client = NoLag(MONITOR_TOKEN);
const monitor = new NoLagAgents({
  client,
  appName: APP_SLUG,
  agentId: "monitor",
  presence: { name: "monitor", role: "observer" },
});
await client.connect();
await monitor.ready();

const observe = new Observe(monitor.room("default-workflow"), monitor.agentId);

// Watch all events in the workflow
observe.on((event) => {
  console.log(`[${event.category}] ${event.severity} from ${event.emittedBy}: ${JSON.stringify(event.payload)}`);
});
```
```python [Python]
from nolag_agents import NoLagAgents, NoLagAgentsOptions, AgentPresenceData
from nolag_agents.patterns import Observe

# In the worker: emit events at the points you want to see
worker_observe = Observe(room, agents.agent_id)
await worker_observe.emit("task:started", {"task_id": task.task_id})

# In a monitor process: watch the stream
monitor = NoLagAgents(MONITOR_TOKEN, NoLagAgentsOptions(
    app_name=APP_SLUG,
    agent_id="monitor",
    presence=AgentPresenceData(name="monitor", role="observer"),
))
await monitor.connect()

observe = Observe(await monitor.room("default-workflow"), monitor.agent_id)

# Watch all events in the workflow
observe.on(lambda event: print(f"[{event.category}] {event.severity} from {event.emitted_by}: {event.payload}"))
```

## Step 7: View in the Agent Dashboard

Open the **Agent Dashboard** in the [portal](https://portal.nolag.app) to see your agents in real time. It shows a topology graph of connected agents (from the `agent-activity` lobby), task counters for what it sees while open (dispatched, in progress, completed, failed), the **Event Stream** from Observe, the **Blackboard State**, the **Approval Queue** of pending Approve requests, and a panel per agent with its role, status, and capabilities. A **Tool Registry** panel is present but stays empty: tool handlers are local to the agent that registers them and are not published anywhere.

## Multi-Tenant Setup

If you are building a multi-tenant application where each customer needs isolated agent workflows, use [Access Scopes](/docs/scopes/getting-started). When an actor is assigned to a scope, the broker rewrites every topic it touches from `app/room/topic` to `app/scope/room/topic`. That includes the coordination topics, so tenant Acme's tasks travel on `my-agents-a3f9/acme/default-workflow/tasks` and its state on `my-agents-a3f9/acme/default-workflow/state`.

This means you deploy one agents app and create scoped actors for each tenant. An orchestrator in tenant A cannot dispatch tasks to workers in tenant B. The isolation is enforced by the broker with no changes to your agent code; the room name and topics you use in the SDK stay the same.

See the [Multi-Tenant Patterns](/docs/scopes/multi-tenancy) guide for detailed implementation examples, including per-tenant agent deployments.

## Next Steps

- [Patterns](/docs/agents/patterns) - deep dive on all six coordination patterns
- [Composition](/docs/agents/composition) - combine agents with chat, notifications, and more
- [Tag Vocabulary](/docs/agents/tags) - learn how capabilities, priority, and tags work
- [Examples](/docs/agents/examples) - canonical recipes for common agent workflows
