Skip to content

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):

C#
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
C#
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:

C#
// 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:

Text Only
POST {SERVER_URL}/api/user/webhooks/builtin

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

Bash
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, and DeleteAsync make HTTP calls to the Xians server. That means they must run either outside a workflow (e.g. in your Program.cs startup, 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

C#
// 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

C#
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

C#
// 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):

C#
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:

C#
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:

C#
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:

C#
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

C#
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