> For the complete documentation index, see [llms.txt](https://dnotifier.gitbook.io/product-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://dnotifier.gitbook.io/product-docs/workflows-and-agents/remote-agents.md).

# Remote agents

Run agents on other servers while keeping the same `runAgent` / `run_agent` model. Register remote targets with a `receiverId` / `receiver_id` on the same Workflow that owns the call.

{% hint style="info" %}
Remote agents need **WebSocket** (`transport: "ws"`) so request/reply messages can flow. Local-only workflows can still use HTTP.
{% endhint %}

## Rules

* Agents are **never** called directly — only a **Workflow** starts the first agent (`entry` → `runAgent` / `run_agent`), and nested calls use the same context APIs.
* A name must be **registered** on that Workflow (local agent or remote receiver) before the call can resolve.

## Register local vs remote

{% tabs %}
{% tab title="JavaScript" %}

```js
workflow.registerAgents({
  "formatter": formatterAgent,
  "outline-planner": { receiverId: "svc-agent-outline" },
});

const outline = await ctx.runAgent("outline-planner", { input: ctx.input });
```

{% endtab %}

{% tab title="Python" %}

```python
workflow.register_agents({
    "formatter": formatter_agent,
    "outline-planner": {"receiver_id": "svc-agent-outline"},
})

outline = await ctx.run_agent("outline-planner", input_data=ctx.input)
```

{% endtab %}

{% tab title="Dart" %}

```dart
workflow.registerAgents({
  'formatter': formatterAgent,
  'outline-planner': RemoteAgentRef(receiverId: 'svc-agent-outline'),
  // or: 'outline-planner': {'receiverId': 'svc-agent-outline'},
});

final outline = await ctx.runAgent('outline-planner', input: ctx.input);
```

{% endtab %}
{% endtabs %}

## Orchestrator

{% tabs %}
{% tab title="JavaScript" %}

```js
import { DNotifier } from "@dnotifier-realtime/dnotifier";

const notifier = new DNotifier({
  appId: process.env.DNOTIFIER_APP_ID,
  secret: process.env.DNOTIFIER_SECRET,
  userId: "svc-workflow",
  transport: "ws",
});

const workflow = new DNotifier.Workflow({
  name: "blog-pipeline",
  observability: true,
  async entry(ctx) {
    const outline = await ctx.runAgent("outline-planner", { input: ctx.input });
    ctx.state.outline = outline;
    const draft = await ctx.runAgent("content-creator", {
      input: { topic: ctx.input.topic, outline },
    });
    return { draft };
  },
}).registerAgents({
  "outline-planner": { receiverId: "svc-agent-outline" },
  "content-creator": { receiverId: "svc-agent-writer" },
});

await notifier.connect();
const run = await notifier.runWorkflow({
  workflow,
  input: { topic: "Realtime agents" },
});
```

{% endtab %}

{% tab title="Python" %}

```python
import os
from dnotifier import DNotifier, define_agent

notifier = DNotifier(
    app_id=os.environ["DNOTIFIER_APP_ID"],
    secret=os.environ["DNOTIFIER_SECRET"],
    user_id="svc-workflow",
    transport="ws",
)

async def entry(ctx):
    outline = await ctx.run_agent("outline-planner", input_data=ctx.input)
    ctx.state["outline"] = outline
    draft = await ctx.run_agent(
        "content-creator",
        input_data={"topic": ctx.input["topic"], "outline": outline},
    )
    return {"draft": draft}

workflow = DNotifier.Workflow(
    name="blog-pipeline",
    observability=True,
    entry=entry,
).register_agents({
    "outline-planner": {"receiver_id": "svc-agent-outline"},
    "content-creator": {"receiver_id": "svc-agent-writer"},
})

await notifier.connect()
run = await notifier.run_workflow(workflow, {"topic": "Realtime agents"})
```

{% endtab %}

{% tab title="Dart" %}

```dart
import 'package:dnotifier/dnotifier.dart';

final notifier = DNotifier(
  appId: Platform.environment['DNOTIFIER_APP_ID']!,
  secret: Platform.environment['DNOTIFIER_SECRET']!,
  userId: 'svc-workflow',
  transport: 'ws',
);

final workflow = Workflow(
  name: 'blog-pipeline',
  observability: true,
  entry: (ctx) async {
    final outline = await ctx.runAgent('outline-planner', input: ctx.input);
    ctx.state['outline'] = outline;
    final draft = await ctx.runAgent('content-creator', input: {
      'topic': ctx.input['topic'],
      'outline': outline,
    });
    return {'draft': draft};
  },
).registerAgents({
  'outline-planner': RemoteAgentRef(receiverId: 'svc-agent-outline'),
  'content-creator': RemoteAgentRef(receiverId: 'svc-agent-writer'),
});

await notifier.connect();
final run = await notifier.runWorkflow(
  workflow: workflow,
  input: {'topic': 'Realtime agents'},
);
```

{% endtab %}
{% endtabs %}

## Agent host

Host local agents on a Workflow, connect with the same `userId` as the orchestrator’s receiver id, then call `listen`.

{% tabs %}
{% tab title="JavaScript" %}

```js
const outlinePlanner = DNotifier.defineAgent({
  name: "outline-planner",
  async run(ctx) {
    // const msg = await ctx.waitForMessage({ timeoutMs: 60_000 });
    const draft = await ctx.sendAI({
      message: { text: `Outline for: ${ctx.input?.topic ?? ctx.input}` },
    });
    ctx.state.plannedAt = Date.now();
    return { outline: draft };
  },
});

const workflow = new DNotifier.Workflow({
  name: "outline-service",
  observability: true,
  entry: async () => {},
}).registerAgents({
  "outline-planner": outlinePlanner,
});

const notifier = new DNotifier({
  appId: process.env.DNOTIFIER_APP_ID,
  secret: process.env.DNOTIFIER_SECRET,
  userId: "svc-agent-outline",
  transport: "ws",
});

await notifier.connect();
workflow.listen(notifier);
```

{% endtab %}

{% tab title="Python" %}

```python
outline_planner = define_agent("outline-planner", outline_run)

workflow = DNotifier.Workflow(
    name="outline-service",
    observability=True,
    entry=lambda ctx: asyncio.sleep(0),
).register_agents({
    "outline-planner": outline_planner,
})

notifier = DNotifier(
    app_id=os.environ["DNOTIFIER_APP_ID"],
    secret=os.environ["DNOTIFIER_SECRET"],
    user_id="svc-agent-outline",
    transport="ws",
)

await notifier.connect()
workflow.listen(notifier)
```

{% endtab %}

{% tab title="Dart" %}

```dart
final outlinePlanner = defineAgent(
  name: 'outline-planner',
  run: (ctx) async {
    // final msg = await ctx.waitForMessage(timeoutMs: 60000);
    final draft = await ctx.sendAI(
      message: {'text': 'Outline for: ${ctx.input}'},
    );
    ctx.state['plannedAt'] = DateTime.now().millisecondsSinceEpoch;
    return {'outline': draft};
  },
);

final workflow = Workflow(
  name: 'outline-service',
  observability: true,
  entry: (_) async {},
).registerAgents({
  'outline-planner': outlinePlanner,
});

final notifier = DNotifier(
  appId: Platform.environment['DNOTIFIER_APP_ID']!,
  secret: Platform.environment['DNOTIFIER_SECRET']!,
  userId: 'svc-agent-outline',
  transport: 'ws',
);

await notifier.connect();
workflow.listen(notifier);
```

{% endtab %}
{% endtabs %}

`listen` answers remote invokes by building a **WorkflowContext** and calling **`runAgent` / `run_agent`** — not by calling the agent object directly.

## Agent → agent (nested)

If agent A calls B and C, register B and C on **the Workflow that owns that `ctx`** (the host Workflow when A runs remotely).

## Mid-run messages

{% tabs %}
{% tab title="JavaScript" %}

```js
async run(ctx) {
  await ctx.sendAI({ message: { text: "Draft ready — reply to approve" } });
  const msg = await ctx.waitForMessage({ timeoutMs: 120_000 });
  const body = msg.payload.toJSON();
  return { approved: Boolean(body?.ok) };
}
```

{% endtab %}

{% tab title="Python" %}

```python
async def run(ctx):
    await ctx.send_ai(message={"text": "Draft ready — reply to approve"})
    msg = await ctx.wait_for_message(timeout_ms=120_000)
    body = msg.payload.to_json()
    return {"approved": bool(body and body.get("ok"))}
```

{% endtab %}

{% tab title="Dart" %}

```dart
run: (ctx) async {
  await ctx.sendAI(message: {'text': 'Draft ready — reply to approve'});
  final msg = await ctx.waitForMessage(timeoutMs: 120000);
  final body = msg.payload.toJSON();
  return {'approved': body is Map && body['ok'] == true};
}
```

{% endtab %}
{% endtabs %}

Unblock with a normal `send` to the host’s `userId`. Remote-agent control messages are handled by the SDK and do not wake `waitForMessage` / `wait_for_message`.

## Shared state

Remote invokes carry `ctx.state`. The host may update state; changes are merged back into the caller’s `ctx.state` when the reply returns.

## Checklist

| Step | Action                                                                           |
| ---- | -------------------------------------------------------------------------------- |
| 1    | Same `appId` / secret on orchestrator and hosts                                  |
| 2    | Host `userId` matches orchestrator `receiverId` / `receiver_id`                  |
| 3    | Both sides use `transport: "ws"` and `connect()`                                 |
| 4    | Host calls `workflow.listen(notifier)`                                           |
| 5    | Every agent name used in `runAgent` / `run_agent` is registered on that Workflow |
