Skip to content

Extending the Agent

Before writing code, ask yourself: can this change be made in rules.json alone? The rules file is the primary extension point and requires no compilation or deployment. See Rules Configuration for the syntax.

If a code change is needed, here’s how.

Activities are the side-effectful steps that workflows orchestrate. The agent’s existing activity class is ContainerActivities.

  1. Add a method to ContainerActivities.cs or create a new class in TheAgent/Activities/:
[Activity]
public async Task<string> MyNewActivityAsync(string input)
{
// side-effectful work here
return result;
}
  1. Register the activity class in XianixAgent.cs (the SDK scans for [Activity] methods on registered types):
xiansAgent.Workflows
.DefineCustom<ProcessingWorkflow>(new WorkflowOptions { Activable = false })
.AddActivity<ContainerActivities>()
.AddActivity<MyNewActivities>(); // add here
  1. Call it from a workflow:
var result = await Workflow.ExecuteActivityAsync(
(MyNewActivities a) => a.MyNewActivityAsync(input),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
  1. Create a class in TheAgent/Workflows/:
[Workflow(Constants.AgentName + ":My New Workflow")]
public class MyNewWorkflow
{
[WorkflowRun]
public async Task WorkflowRun(string input)
{
// orchestrate activities here
}
}
  1. Register it in XianixAgent.cs alongside the existing workflows.

  2. Start it from another workflow:

await XiansContext.Workflows.StartAsync<MyNewWorkflow>(
new object[] { input }, Guid.NewGuid().ToString());

Workflows must be deterministic — the same inputs must always produce the same commands. This means:

  • Don’t use DateTime.Now, Guid.NewGuid(), or Random directly. Use Workflow.UtcNow, Workflow.NewGuid(), Workflow.Random.
  • Don’t call external services from a workflow. Put all I/O in activities.
  • Don’t use non-Temporal async/await. Use Workflow.ExecuteActivityAsync or Workflow.WaitConditionAsync.

All environment variable access goes through EnvConfig.cs:

public static string MyNewSetting => Get("MY_NEW_SETTING", "default-value");

Add the variable to .env.example too.

New services are registered in Program.cs inside ConfigureServices():

services.AddSingleton<IMyService, MyService>();

The test project lives at TheAgent.Tests/ and uses xUnit with NSubstitute for mocking.

Terminal window
dotnet test TheAgent.Tests/TheAgent.Tests.csproj

Tests follow a standard Arrange / Act / Assert pattern. Mocks are created via Substitute.For<TInterface>():

public class MyComponentTests
{
private readonly IMyDependency _dep = Substitute.For<IMyDependency>();
private readonly MyComponent _sut;
public MyComponentTests()
{
_sut = new MyComponent(_dep);
}
[Fact]
public async Task DoWork_WhenInputValid_ReturnsExpected()
{
_dep.GetValueAsync().Returns(Task.FromResult("hello"));
var result = await _sut.DoWork();
Assert.Equal("hello", result);
}
}
ComponentFocus
WebhookRulesEvaluatorFilter matching, input extraction, path resolution, edge cases
EventOrchestratorHandled vs. ignored results, input propagation, exceptions
EnvConfigRequired-variable validation, fallback defaults, tenant-scoped keys
ContainerActivitiesIntegration tests or manual testing (requires Docker)
  • Mirror the source folder structure under TheAgent.Tests/
  • Name tests MethodName_Condition_ExpectedOutcome
  • Use Substitute.For<T>() for dependencies, real instances for the SUT

Ready to ship? See Deployment for Docker publishing and Azure setup.