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.
Adding a New Activity
Section titled “Adding a New Activity”Activities are the side-effectful steps that workflows orchestrate. The agent’s existing activity class is ContainerActivities.
- Add a method to
ContainerActivities.csor create a new class inTheAgent/Activities/:
[Activity]public async Task<string> MyNewActivityAsync(string input){ // side-effectful work here return result;}- 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- Call it from a workflow:
var result = await Workflow.ExecuteActivityAsync( (MyNewActivities a) => a.MyNewActivityAsync(input), new ActivityOptions { StartToCloseTimeout = TimeSpan.FromMinutes(5) });Adding a New Workflow
Section titled “Adding a New Workflow”- 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 }}-
Register it in
XianixAgent.csalongside the existing workflows. -
Start it from another workflow:
await XiansContext.Workflows.StartAsync<MyNewWorkflow>( new object[] { input }, Guid.NewGuid().ToString());Temporal Determinism Rules
Section titled “Temporal Determinism Rules”Workflows must be deterministic — the same inputs must always produce the same commands. This means:
- Don’t use
DateTime.Now,Guid.NewGuid(), orRandomdirectly. UseWorkflow.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. UseWorkflow.ExecuteActivityAsyncorWorkflow.WaitConditionAsync.
Adding New Configuration
Section titled “Adding New Configuration”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.
Registering New Services
Section titled “Registering New Services”New services are registered in Program.cs inside ConfigureServices():
services.AddSingleton<IMyService, MyService>();Testing
Section titled “Testing”The test project lives at TheAgent.Tests/ and uses xUnit with NSubstitute for mocking.
Running Tests
Section titled “Running Tests”dotnet test TheAgent.Tests/TheAgent.Tests.csprojWriting Tests
Section titled “Writing Tests”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); }}What to Test
Section titled “What to Test”| Component | Focus |
|---|---|
WebhookRulesEvaluator | Filter matching, input extraction, path resolution, edge cases |
EventOrchestrator | Handled vs. ignored results, input propagation, exceptions |
EnvConfig | Required-variable validation, fallback defaults, tenant-scoped keys |
ContainerActivities | Integration tests or manual testing (requires Docker) |
Conventions
Section titled “Conventions”- Mirror the source folder structure under
TheAgent.Tests/ - Name tests
MethodName_Condition_ExpectedOutcome - Use
Substitute.For<T>()for dependencies, real instances for the SUT
Next Step
Section titled “Next Step”Ready to ship? See Deployment for Docker publishing and Azure setup.