Webhooks¶
Why Webhooks?¶
Users aren't the only ones who need to talk to your agent. Payment processors, CRMs, email services, and monitoring systems push events over HTTP. Webhooks give every agent an HTTP entry point: an external system POSTs to the Xians server, the server routes the request to your workflow's handler, and your handler's response travels back as the HTTP response.
sequenceDiagram
participant E as External system
participant S as Xians Server
participant W as Integrator Workflow
E->>S: POST /api/user/webhooks/builtin (JSON payload)
S->>W: Route to OnWebhook handler
W->>W: Process payload
W-->>S: context.Respond(...)
S-->>E: HTTP response (status, headers, body) Handling Webhooks¶
Register an OnWebhook handler on a built-in workflow. DefineIntegrator() creates the conventionally-named Integrator Workflow — the name Agent Studio's default webhook endpoint targets (see Workflow Naming Conventions):
var integratorWorkflow = xiansAgent.Workflows.DefineIntegrator();
// Synchronous handler
integratorWorkflow.OnWebhook((context) =>
{
Console.WriteLine($"Received: {context.Webhook.Name}");
context.Respond(new { status = "success" });
});
// Async handler — for database calls, HTTP requests, etc.
integratorWorkflow.OnWebhook(async (context) =>
{
var result = await ProcessWebhookAsync(context.Webhook.Payload);
context.Respond(new { status = "success", result });
});
Reading the Webhook¶
Everything about the incoming request is on context.Webhook:
| Property | Type | Description |
|---|---|---|
Name | string | Webhook name from the query parameter |
ParticipantId | string | Participant identifier |
Payload | string? | Request body as a JSON string — deserialize it yourself |
Scope | string? | Optional scope context |
Authorization | string? | Optional auth token for your own validation |
RequestId | string | Unique request ID (useful for logging) |
TenantId | string | Tenant context |
integratorWorkflow.OnWebhook((context) =>
{
if (string.IsNullOrEmpty(context.Webhook.Payload))
{
context.Response = WebhookResponse.BadRequest("Missing payload");
return;
}
var payload = JsonSerializer.Deserialize<OrderPayload>(context.Webhook.Payload);
Console.WriteLine($"Processing order {payload!.OrderId} for ${payload.Amount}");
context.Respond(new { accepted = true });
});
Responding¶
Three styles, from simplest to most control:
// 1. Object — auto-serialized to JSON, 200 OK
context.Respond(new { message = "Success", processedAt = DateTime.UtcNow });
// 2. Factory methods — common statuses without boilerplate
context.Response = WebhookResponse.Ok(new { success = true });
context.Response = WebhookResponse.BadRequest("Missing required field: orderId");
context.Response = WebhookResponse.NotFound("Order not found");
context.Response = WebhookResponse.InternalServerError("Processing failed");
context.Response = WebhookResponse.Error(HttpStatusCode.Unauthorized, "Invalid token");
// 3. Full control — custom status, headers, content type
context.Response = new WebhookResponse
{
StatusCode = HttpStatusCode.OK,
Content = "{\"message\": \"Success\"}",
ContentType = "application/json",
Headers = new Dictionary<string, string[]>
{
["X-Request-Id"] = new[] { context.Webhook.RequestId }
}
};
Calling Your Webhook¶
External systems POST to:
Query Parameters¶
| Parameter | Required | Description |
|---|---|---|
apikey | Yes | Your Xians API key |
agentName | Yes | Target agent name |
workflowName | Yes | Target workflow name (e.g. Integrator Workflow) |
webhookName | Yes | Your identifier for this event (e.g. OrderCompleted) |
participantId | Yes | User/participant identifier |
activationName | No | Target a specific workflow instance |
timeoutSeconds | No | Request timeout (default 30s) |
scope | No | Scope context, same semantics as chat messages |
authorization | No | Token your handler can validate |
The JSON request body becomes context.Webhook.Payload.
Example¶
curl -X POST "http://localhost:5005/api/user/webhooks/builtin?apikey=sk-Xnai-abc123&agentName=WebhookTestAgent&workflowName=Integrator%20Workflow&webhookName=OrderCompleted&participantId=customer@example.com" \
-H "Content-Type: application/json" \
-d '{"orderId": "12345", "amount": 99.99, "status": "completed"}'
Managing Webhooks Programmatically¶
Everything above assumes a webhook endpoint already exists (for example, the default one Agent Studio creates). But agents can also create, list, and delete their own inbound webhooks at runtime through the SDK — no Studio or admin API required. Each created webhook is a real trigger: it provisions an API key and returns a ready-to-call WebhookUrl.
The API lives on agent.Webhooks and resolves the agent name and activation automatically from the current context, so you rarely pass them in.
Where can I call these?
CreateAsync,ListAsync, andDeleteAsyncmake HTTP calls to the Xians server. That means they must run either outside a workflow (e.g. in yourProgram.csstartup, or a plain service) or inside a Temporal activity — never directly in deterministic workflow code. To manage webhooks as part of a workflow, call them from an activity that the workflow invokes.
Creating a Webhook¶
// agentName + activationName are resolved from context automatically
var webhook = await agent.Webhooks.CreateAsync(
webhookName: "OrderReceived",
name: "Order received trigger");
Console.WriteLine(webhook.Id); // use this to delete it later
Console.WriteLine(webhook.WebhookUrl); // the URL external systems should POST to
CreateAsync parameters (all optional):
| Parameter | Default (server) | Description |
|---|---|---|
webhookName | Default | The webhook name/scope used when triggering |
workflowName | Integrator Workflow | The target workflow the webhook delivers to |
participantId | webhook | The participant id the webhook runs as |
timeoutSeconds | 30 | Synchronous response timeout (1–300) |
name | — | Human-readable label for the webhook |
activationName | current activation | Override the target activation instead of using context |
Listing Webhooks¶
var webhooks = await agent.Webhooks.ListAsync();
foreach (var w in webhooks)
{
Console.WriteLine($"{w.WebhookName}: {w.WebhookUrl} (enabled={w.IsEnabled})");
}
When called inside a workflow/activity, ListAsync is scoped to the current activation; when called outside any activation context it returns all activations of the agent.
Each WebhookInfo exposes Id, Name, AgentName, ActivationName, WorkflowId, WebhookUrl, IsEnabled, CreatedAt, plus convenience accessors WebhookName, WorkflowName, ParticipantId, and TimeoutInSeconds.
Deleting a Webhook¶
// Revokes the underlying API key and removes the trigger. Returns false if not found.
var deleted = await agent.Webhooks.DeleteAsync(webhook.Id);
Checking "Self" Information¶
To confirm an activation exists and is active before acting on it (for example, before creating a webhook for it):
bool exists = await agent.ActivationExistsAsync(); // current activation
var status = await agent.GetActivationStatusAsync(); // Active | NotFound | Deactivated
For checking or managing other agents' activations in the tenant (agent.Tenant.Agent(...)), see Agents & Activations.
Doing it from a Workflow (via an activity)¶
Because the calls do I/O, put them in an activity and let the workflow orchestrate:
public class WebhookManagementActivities
{
[Activity]
public async Task<string> CreateWebhookAsync()
{
// agent + activation resolved from the activity's context
var agent = XiansContext.CurrentAgent;
var webhook = await agent.Webhooks.CreateAsync(webhookName: "OrderReceived");
return webhook.Id;
}
}
// In the workflow:
var id = await Workflow.ExecuteActivityAsync(
(WebhookManagementActivities a) => a.CreateWebhookAsync(),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(1) });
A full, runnable end-to-end sample — a Default webhook that starts a custom workflow which creates, lists, waits, then deletes webhooks — lives in the WebhookManagement example project.
Common Patterns¶
Validate authorization¶
The platform authenticates the API key, but the optional authorization token is yours to validate:
integratorWorkflow.OnWebhook(async (context) =>
{
var authToken = context.Webhook.Authorization;
if (string.IsNullOrEmpty(authToken) || !await ValidateTokenAsync(authToken))
{
context.Response = WebhookResponse.Error(
HttpStatusCode.Unauthorized, "Invalid or missing authorization token");
return;
}
// ... process
});
Accept fast, process in a workflow¶
Webhook callers usually expect a quick acknowledgment. For long-running work, accept immediately and start a durable workflow:
integratorWorkflow.OnWebhook(async (context) =>
{
var payloadJson = context.Webhook.Payload;
// Start a durable workflow instead of Task.Run — workflows must stay deterministic
await XiansContext.Workflows.StartAsync<ProcessOrderWorkflow>(
new object[] { payloadJson });
context.Respond(new
{
status = "accepted",
requestId = context.Webhook.RequestId
});
});
Handle errors with meaningful status codes¶
integratorWorkflow.OnWebhook(async (context) =>
{
try
{
var result = await ProcessOrderAsync(context.Webhook.Payload);
context.Response = WebhookResponse.Ok(new { success = true, orderId = result.OrderId });
}
catch (ValidationException ex)
{
context.Response = WebhookResponse.BadRequest(ex.Message);
}
catch (Exception ex)
{
context.Response = WebhookResponse.InternalServerError($"Failed: {ex.Message}");
}
});
Next Steps¶
- Agents & Workflows — built-in workflows and system-scoped agents
- Replying to Users — the conversational messaging side