Replying to User Messages¶
Why Message Handlers?¶
Conversational agents spend most of their time doing one thing: receiving a user message and responding. Xians built-in workflows make this a one-liner — you register a listener, and every incoming message arrives as a rich UserMessageContext that already knows who sent it, which conversation it belongs to, and how to reply. You never manage connections, routing, or conversation state yourself.
Replying vs. proactive messaging
This page covers responding to messages users send you. To initiate a message from a background workflow (notifications, alerts), use XiansContext.Messaging — see Proactive Messaging.
Registering Listeners¶
Register a handler per message type on a built-in workflow:
var conversationalWorkflow = xiansAgent.Workflows.DefineSupervisor();
// Chat (plain text)
conversationalWorkflow.OnUserChatMessage(async (context) =>
{
await context.ReplyAsync($"You said: {context.Message.Text}");
});
// Data (structured payloads)
conversationalWorkflow.OnUserDataMessage(async (context) =>
{
var data = context.Message.Data;
await context.ReplyAsync("Data received and processed!");
});
// File uploads — see the File Upload page for details
conversationalWorkflow.OnFileUpload(async (context) =>
{
foreach (var file in context.Message.Files)
{
var bytes = file.GetBytes();
}
await context.ReplyAsync("File received!");
});
DefineSupervisor()creates a built-in workflow namedSupervisor Workflow— the name Agent Studio connects to by default for user chat. See Workflow Naming Conventions.
The Incoming Message¶
Everything about the incoming message is on context.Message:
| Property | Type | Description |
|---|---|---|
Text | string | Text content |
Data | object? | Structured payload |
Files | IReadOnlyList<UploadedFile> | Decoded files (for File messages) |
ParticipantId | string | Who sent it |
ThreadId | string? | Conversation thread |
Scope | string? | Topic within the thread (see below) |
RequestId | string | Unique ID of this request |
TenantId | string | Tenant (multi-tenancy) |
Hint | string? | Optional handling hint |
Authorization | string? | Auth token if provided |
Ways to Respond¶
Pick the method based on what the primary content is:
| Method | Primary content | Example use |
|---|---|---|
ReplyAsync(text) | Text | Normal conversation |
ReplyAsync(text, data) | Text + attached data | "Order ready!" plus order details |
SendDataAsync(data, text?) | Data | Charts, structured results |
await context.ReplyAsync("Hello! How can I help?");
await context.ReplyAsync("Processing complete!", new { ProcessedItems = 42 });
await context.SendDataAsync(analyticsData, "Here are your analytics");
Skipping the Response¶
Set context.SkipResponse = true to process a message silently (analytics, logging, background processing):
if (context.Message.Text.StartsWith("LOG:"))
{
context.SkipResponse = true;
await LogSystemEvent(context.Message.Text);
return;
}
Threads and Scopes: How Conversations Are Organized¶
Why threads?¶
Every user needs their own private, persistent conversation with each workflow. Xians handles this with threads: one thread per (Tenant, Workflow, Participant) combination. You never create threads — they exist automatically, persist forever, and are fully isolated from each other.
Why scopes?¶
A single thread can cover many subjects — an order issue, a billing question, general chat. Scopes partition a thread into isolated topics, so history for one topic never bleeds into another.
Thread (Tenant + Workflow + Participant)
├── Scope: null ← general conversation
│ ├── Message 1
│ └── Message 2
├── Scope: "Order #12345" ← isolated topic
│ ├── Message 3
│ └── Message 4
└── Scope: "Technical Support"
└── Message 5
Think of it as: thread = the book, scope = a chapter, messages = the pages.
Scope rules¶
- Replies automatically inherit the scope of the incoming message — topic continuity is free.
- Chat history is scope-isolated:
GetChatHistoryAsync()returns only messages in the current scope. nullscope is itself an isolated context (the "general" conversation).- Use human-readable scope values (they may be shown to users):
"Order #12345 - Delivery Status", not"ord_12345_dlv".
conversationalWorkflow.OnUserChatMessage(async (context) =>
{
var scope = context.Message.Scope;
// Returns only messages sharing this message's scope
var history = await context.GetChatHistoryAsync(pageSize: 20);
// This reply automatically keeps the same scope
await context.ReplyAsync(scope != null
? $"Continuing our discussion about '{scope}'."
: "General conversation.");
});
Chat History¶
Use history to give context-aware responses. It's paginated — fetch only what you need:
Avoid fetching very large pages in real-time handlers; a small pageSize (5–20) is almost always enough.
Hints, Task IDs, and Metadata¶
// Hint from the incoming message (user intent / routing context)
var hint = context.Message.Hint;
// Last HITL task linked to this conversation (see Human-in-the-Loop)
var lastTaskId = await context.GetLastTaskIdAsync();
// Metadata set internally by the platform — never comes from the user
if (context.Metadata?.TryGetValue("priority", out var priority) == true && priority == "high")
{
await HandleUrgentMessage(context);
}
Best Practices¶
- Handle errors gracefully — wrap processing in try/catch and reply with a friendly message on failure; never leave the user without a response.
- Be specific in replies — "I've updated your preferences; daily summaries arrive at 9 AM" beats "Done".
- Match the method to the content — text goes in
ReplyAsync, data-first responses inSendDataAsync. - Keep scopes human-readable — they double as topic labels in Agent Studio.
Next Steps¶
- Messaging – Progress — stream "working on it" updates before the final reply
- Messaging – File Upload — handle files from users
- Proactive Messaging — initiate messages from background workflows