# Chat (Text-based Conversation) Nodes — Implementation

Wire whatever shape the conversation needs, inside the rules in [planning.md](planning.md#critical-rules-any-conversational-flow-must-follow).

For a phone conversation, use [inline-voice-agent/impl.md](../inline-voice-agent/impl.md) instead.

Everything here is the same for all three agent flavors except the agent node itself and its port. Settle the flavor first — [planning.md](planning.md#pick-the-agent-flavor-before-you-build).

## Resolve the Agent

**Inline** — scaffold it, from the solution root:

```bash
uip agent init "<FlowProjectName>" --inline-in-flow --conversational
```

Writes `<FlowProject>/<uuid>/agent.json` plus `flow-layout.json`, and returns `Data.ProjectId`. **Keep that UUID** — it is what the agent node's `inputs.source` must carry.

**In-solution** — the agent is a sibling project; nothing to scaffold in the flow. Find its node type:

```bash
uip maestro flow registry list --local --output json
```

**Published** — already on the tenant:

```bash
uip maestro flow registry search "<agent name>" --output json
```

Both of the latter give a `uipath.core.agent.*` node type — suffixed with the solution resource key in-solution, the Orchestrator-assigned UUID when published. `registry get` on it returns `inputDefaults` holding `isConversational: true` and `conversationalAgentSettings`, which is how you confirm the agent really is a chat agent rather than an autonomous one. Discovery details live in [agent/impl.md](../agent/impl.md#discovery-and-registry-validation).

**If an in-solution agent comes back autonomous**, the registry could not read its `agent.json` — it builds that node from the sibling project's file, and falls back to autonomous when the file is missing or malformed. Re-run with `--log-level debug` and it names the reason:

```
[DEBUG] Unparseable agent.json in <projectDir>: … The node will describe an autonomous agent.
[DEBUG] No readable agent.json in <projectDir>; the node will describe an autonomous agent.
```

Nothing else reports it, and the authored node would run as autonomous with no error at any step. Published agents are unaffected — their flag comes from the release, not a local file.

## Configure `agent.json`

Edit the scaffolded file, then regenerate its derived fields:

```bash
uip agent refresh "<FlowProject>/<uuid>" --inline-in-flow
uip agent validate "<FlowProject>/<uuid>" --inline-in-flow
```

`refresh` rebuilds `contentTokens[]` from `messages[].content`. **Skip it and `agent validate` fails** with `contentTokens has 0 entries but content requires 1` — the error does not name `agent refresh`, so it is easy to get stuck on.

Run `refresh` after any `agent.json` edit — for an in-solution agent too, without the flag: `uip agent refresh "<AgentProject>"`. A published agent has no local file, so there is nothing to refresh.

Settings that matter for a conversational agent:

| Key | Value | Why |
| --- | --- | --- |
| `settings.engine` | `conversational-v1` | What makes it a chat agent rather than autonomous |
| `metadata.isConversational` | `true` | Read by the registry to pick the icon and keep the agent out of the agent-as-tool picker |
| `settings.maxIterations` | `8` | keep what the scaffold wrote |

`uip agent init --conversational` writes all three. Do not remove them.

## Registry Validation

`flow validate` has **no registry fallback** — a hand-authored node must carry its manifest in the file's `definitions[]`. Fetch one per node type in the flow:

```bash
uip maestro flow registry get core.trigger.conversation --output json
uip maestro flow registry get uipath.conversational.wait-for-message --output json
uip maestro flow registry get uipath.agent.conversational --output json
```

Append each `Data.Node` verbatim to the `.flow`'s `definitions[]`, and set the node's `typeVersion` to exactly the `version` the command returned. Miss one and validate reports `Node type "<type>:<version>" has no matching definition`.

The conversational agent node requires a login; the trigger and the tool nodes resolve from the bundled catalog.

## The `conversationalAgentSettings` Wiring Rule

This is the one that goes wrong silently. The agent reads the conversation through `inputs.conversationalAgentSettings`, which holds **five** keys — a `context` binding plus four fields derived from it:

```json
"conversationalAgentSettings": {
  "mode": "simple",
  "context":        { "type": "jsExpression", "expression": "$vars.waitForMessage1.output.conversationContext",                  "fieldType": "object" },
  "conversationId": { "type": "jsExpression", "expression": "$vars.waitForMessage1.output.conversationContext.conversationId",   "fieldType": "string" },
  "exchangeId":     { "type": "jsExpression", "expression": "$vars.waitForMessage1.output.conversationContext.latestExchangeId", "fieldType": "string" },
  "messages":       { "type": "jsExpression", "expression": "$vars.waitForMessage1.output.conversationContext.messages",         "fieldType": "array"  },
  "userSettings":   { "type": "jsExpression", "expression": "$vars.waitForMessage1.output.conversationContext.userSettings",     "fieldType": "object" }
}
```

**`mode` and `context` exist only to restore the editor UI.** The four individual fields are what the runtime reads, in either mode, and validation requires `conversationId` regardless. `custom` is for a turn assembled from more than one source. `mode` is optional; the examples here all declare `simple`.

**Write all five.** In Studio Web the author fills `context` and the panel derives the other four, but that derivation only runs in the editor — nothing derives them when the file is authored from the CLI.

Validation only requires `conversationId`, so it half-helps: leave that out and validate fails, but bind `context` and `conversationId` while dropping `exchangeId`, `messages` and `userSettings` and validate passes. The runtime reads all four, so that flow ships an agent with no chat history and no user settings.

Note the field is `latestExchangeId` inside `conversationContext`, not `exchangeId`.

## Bindings Are Objects, Not `=js:` Strings

Every expression binding in a `.flow` is the object form above. A bare `"=js:$vars.…"` string is a pre-1.3 file format that current files no longer use — Studio Web renders it as literal text rather than a binding, and nothing warns.

This matters when using `node add`, which writes `--input` JSON through untouched:

```bash
# WRONG — lands in the file verbatim, renders as text
uip maestro flow node add ChatFlow/ChatFlow.flow uipath.conversational.wait-for-message \
  --input '{"conversationId":"=js:$vars.conversationTrigger1.output.conversationId"}'

# RIGHT
uip maestro flow node add ChatFlow/ChatFlow.flow uipath.conversational.wait-for-message \
  --input '{"conversationId":{"type":"jsExpression","expression":"$vars.conversationTrigger1.output.conversationId","fieldType":"string"}}'
```

(`uip maestro flow node configure --detail` uses the `=js:` form for **connector** nodes — that is a different surface and does not apply here.)

## Node JSON

> **Which surface authors this node.** [inline-agent/impl.md § What NOT to Do](../inline-agent/impl.md#what-not-to-do) bars Flow CLI `node add` / `edge add` for inline **autonomous** agent graph edits. That rule does not cover `uipath.agent.conversational`: the CLI path below is the supported one for this node, `--source` is a documented `node add` flag for it, and the full recipe validates clean. Use `Edit` / `Write` when you prefer, but do not read the inline-agent prohibition as applying here.

Editing the `.flow` directly carries the usual obligations — chiefly a `variables.nodes[]` entry for every data-producing node, which is what makes `$vars.<id>.output` resolve at all. See [editing-operations-json.md](../../editing-operations-json.md). `node add` writes those entries for you.

### Conversation trigger

Replace the default manual trigger — `flow init` scaffolds `core.trigger.manual`, and the conversation trigger is what makes the packaged flow conversational and exposed as a chat experience. Same two-step shape as [editing-operations-cli.md § Replace manual trigger with connector trigger](../../editing-operations-cli.md#replace-manual-trigger-with-connector-trigger), with the conversation trigger in place of a connector one.

```bash
uip maestro flow node remove ChatFlow/ChatFlow.flow start
uip maestro flow node add ChatFlow/ChatFlow.flow core.trigger.conversation --position 256,144
```

### Wait for message

```json
{
  "id": "waitForMessage1",
  "type": "uipath.conversational.wait-for-message",
  "inputs": {
    "conversationId": { "type": "jsExpression", "expression": "$vars.conversationTrigger1.output.conversationId", "fieldType": "string" },
    "from": "User"
  }
}
```

### The agent — inline

`inputs.source` is the scaffolded UUID; `conversationalAgentSettings` is the five-key block above.

```bash
uip maestro flow node add ChatFlow/ChatFlow.flow uipath.agent.conversational \
  --position 768,144 --source <ProjectId> --input '<the settings JSON>'
```

### The agent — in-solution or published

Same settings block, different node type, and `isConversational` alongside it instead of `source`. The suffix is the solution resource key in-solution and the Orchestrator-assigned UUID when published, so read the exact `nodeType` off `registry get` either way:

```json
{
  "id": "supportAgent1",
  "type": "uipath.core.agent.<suffix from registry get>",
  "typeVersion": "<version from registry get>",
  "inputs": {
    "isConversational": true,
    "conversationalAgentSettings": { "...": "the five-key block above" }
  }
}
```

An in-solution agent needs its `definitions[]` entry fetched with `--local`; a published one comes from the pulled tenant registry.

### Send message (for flow-composed messages)

`conversationId`, `exchangeId`, `content`, `role` and `mimeType` are all required. `role` and `mimeType` each accept exactly one value, so write them as shown. `content` is normally a literal — the agent's own replies are streamed, not routed through this node.

```json
{
  "id": "sendMessage1",
  "type": "uipath.conversational.send-message",
  "inputs": {
    "conversationId": { "type": "jsExpression", "expression": "$vars.conversationTrigger1.output.conversationId", "fieldType": "string" },
    "exchangeId":     { "type": "jsExpression", "expression": "$vars.waitForMessage1.output.conversationContext.latestExchangeId", "fieldType": "string" },
    "content":        { "type": "literal", "expression": "Anything else I can help with?", "fieldType": "string" },
    "role": "assistant",
    "mimeType": "text/markdown"
  }
}
```

### Get conversation context

Reads recent exchanges without waiting. Rarely needed, and constrained — see [planning.md § Get Conversation Context](planning.md#get-conversation-context).
```json
{
  "id": "getConversationContext1",
  "type": "uipath.conversational.get-conversation-context",
  "inputs": {
    "conversationId": { "type": "jsExpression", "expression": "$vars.conversationTrigger1.output.conversationId", "fieldType": "string" },
    "exchangeLimit": 20
  }
}
```

## Structured Outputs

In addition to responding to the chat, an **inline** conversational agent can also return named fields for a downstream node to route on. Imported standalone conversational agents (published or in-solution) do not support structured output fields.

Declare each field in two places or it yields nothing at run time:

| Where | What |
| --- | --- |
| the node, in the `.flow` | `inputs.agentOutputVariables: [{ "id": "shouldHandoff", "type": "boolean", "description": "..." }]` |
| the inline `agent.json` | the same field under `outputSchema.properties` |

Bind it downstream as `$vars.<agentNodeId>.output.shouldHandoff`. Writing one side without the other passes `agent validate` and `flow validate` — nothing checks the pair.

### Prompting: what goes where

The prompts that drive the agent's chat replies and structured output differ:

| Generated | Driven by |
| --- | --- |
| the chat reply | the system prompt **only** |
| the structured outputs | the system prompt **plus** each field's `description` |

So split the instructions by destination — *what to say* and *response instructions* in the system prompt, *how to fill the field* in that field's `description` (write the same description in both `agentOutputVariables[]` and `outputSchema.properties`):

| Where | Example |
| --- | --- |
| system prompt | "Thank the user when they would like to end the conversation." |
| `endConversation` (boolean) `description` | "Set to true when the user intends to end the conversation." |

**Do not name the output field and how to set it in the system prompt.** An instruction like "set `endConversation` to `true` when the user says goodbye" in the system prompt may make the LLM emit the structured value to the chat or look for a tool to set the output variables.

## Wire the Edges

An **inline** agent leaves on `success`; an in-solution or published one leaves on `output`. The smallest loop:

```bash
uip maestro flow edge add ChatFlow/ChatFlow.flow conversationTrigger1 waitForMessage1
uip maestro flow edge add ChatFlow/ChatFlow.flow waitForMessage1 conversationalAgent1
uip maestro flow edge add ChatFlow/ChatFlow.flow conversationalAgent1 waitForMessage1 --source-port success
```

Omit `--source-port success` and the command fails with `Source port "output" not found on node "conversationalAgent1". Available source ports: escalation, context, tool, success`.

## Validate

```bash
uip maestro flow validate ChatFlow/ChatFlow.flow
```

Unbound identifiers each report their own error, naming the node and field:

```
[nodes[waitForMessage1].inputs.conversationId]   [SCHEMA_ERROR] Conversation ID is required
[nodes[sendMessage1].inputs.exchangeId]          [SCHEMA_ERROR] Exchange ID is required
[nodes[sendMessage1].inputs.content]             [SCHEMA_ERROR] Content is required
[nodes[conversationalAgent1].inputs.conversationalAgentSettings.conversationId]
                                                 [CONVERSATIONAL_CONVERSATION_ID_REQUIRED] Conversation ID is required
```

A clean validate does **not** mean the bindings are right — see [planning.md § Output Variables](planning.md#output-variables).

## Pack and Ship

```bash
uip maestro flow pack ChatFlow ./dist --version 1.0.0
```

Confirm the marker in the packaged `content/operate.json`:

```json
"runtimeOptions": { "isConversational": true }
```

The marker is the contract with the runtime chat channels: a headless SDK lists any deployed process carrying it, and every OOTB integration on that SDK (e.g. web-chat, iframe embedding, UiPath Assistant, Microsoft Teams, Slack) plus custom UIs pick it up automatically.

Absent means the flow does not start on `core.trigger.conversation`, and it will not be listed as a Conversational Agent in the channels.

## Debug — the CLI Hands Off

A chat cannot be driven headlessly, so `flow debug` uploads the solution and stops:

```bash
uip maestro flow debug ChatFlow --open-in-browser
```

Returns `Code: FlowDebugStudioWebHandoff` with `Data.studioWebUrl` and `Data.handedOff: true`, and no debug session is started. `--timeout` has no effect on this path.

Two chat UIs can drive the run — the CLI names both:

- **Studio Web** — open `Data.studioWebUrl` (`--open-in-browser` does it for you) and chat from its panel
- **UiPath Maestro VS Code extension** — open the flow and hit Debug

If the solution's `.uipx` already carries a `SolutionId`, the upload overwrites that solution rather than creating a second one.

## What NOT to Do

- **Do not stop at `context` and `conversationId`** — see [§ The `conversationalAgentSettings` Wiring Rule](#the-conversationalagentsettings-wiring-rule).
- **Do not invent output paths.** `waitForMessage1.output.exchangeId` and `conversationalAgent1.output.response` do not exist, and validate accepts both.
- **Do not use `=js:` strings** for bindings in a `.flow`.
- **Do not carry one flavor's agent port across.** Inline continues on `success`, in-solution and published on `output`.
- **Do not add a send-message just to deliver the conversational agent's reply.** The conversational agent already streams it.
- **Do not expect `flow debug` to run the conversation.** It hands off to Studio Web or the VS Code extension.
- **Do not leave the manual trigger in place.** Without `core.trigger.conversation` the package is not marked conversational, and nothing errors.
