Capabilities and Routing
Not every worker can execute every module. Some modules need Docker, others need a specific OS. The capability system controls how modules are routed to the right worker.
Worker Capabilities
Workers advertise typed Capability values when they register with the coordinator. Built-in values provide discoverable names, while implicit string conversion still supports custom capabilities.
builder.AddDistributedMode(o =>
{
o.InstanceIndex = 1;
o.TotalInstances = 4;
o.Capabilities = [Capability.Docker, Capability.Gpu, "high-memory"];
});
Auto-Detected OS Capability
By default, AutoDetectOsCapability is true, which automatically adds the current operating system as a capability:
- Windows runners advertise
Capability.Windows - Linux runners advertise
Capability.Linux - macOS runners advertise
Capability.MacOS - FreeBSD runners advertise
Capability.FreeBSD
Attribute arguments must be compile-time constants, so use the corresponding Capability.Names values. For example, modules with [RequiresCapability(Capability.Names.Linux)] only run on Linux workers without extra configuration.
Auto-Detected OS from Platform Conditions
When a module has a [RunIf<OnLinux>], [RunIf<OnWindows>], [RunIf<OnMacOS>], or [RunIf<OnFreeBSD>] attribute, the framework automatically adds the corresponding OS capability requirement to its assignment. This keeps the attribute set DRY — you don't need to add both [RunIf<OnLinux>] and [RequiresCapability(Capability.Names.Linux)] to the same module.
// The "linux" capability is auto-detected — no [RequiresCapability] needed
[RunIf<OnLinux>]
public class LinuxBuildModule : Module<string>
{
protected override async Task<string> ExecuteAsync(
IModuleContext context, CancellationToken cancellationToken)
{
// Only executes on workers that have the "linux" capability
return "built on linux";
}
}
RequiresCapability Attribute
Mark a module with [RequiresCapability] to restrict which workers can execute it. The module will only be assigned to workers that have all required capabilities.
[RequiresCapability(Capability.Names.Docker)]
public class DockerBuildModule : Module<string>
{
protected override async Task<string> ExecuteAsync(
IModuleContext context, CancellationToken cancellationToken)
{
// Only executes on workers that advertise "docker"
await context.Tools.Docker.BuildAsync(new());
return "built";
}
}
Multiple Capabilities
Pass multiple names to one attribute or stack attributes. Both forms require all declared capabilities:
[RequiresCapability(Capability.Names.Linux, Capability.Names.Docker)]
public class LinuxDockerModule : Module<string>
{
protected override async Task<string> ExecuteAsync(
IModuleContext context, CancellationToken cancellationToken)
{
// Only runs on Linux workers that also have Docker
return "done";
}
}
No Capabilities
Modules without [RequiresCapability] can run on any worker. They have no routing restrictions.
Capability Matching Rules
The matching logic is straightforward:
- If a module has no required capabilities, it can run on any worker.
- If a module has required capabilities, all of them must be present in the worker's capability set.
- Capability matching is case-insensitive.
- If no worker with the required capabilities is available, only that module waits in the queue. After
CapabilityTimeout, it fails with a routing error that lists the missing route instead of waiting for the module-result timeout.
Example: Mixed Pipeline
// Runs on any worker (including the master)
public class RestoreModule : Module<string> { ... }
// Only on Linux workers (auto-detected from [RunIf<OnLinux>])
[RunIf<OnLinux>]
[DependsOn<RestoreModule>]
public class LinuxBuildModule : Module<string> { ... }
// Only on Windows workers (auto-detected from [RunIf<OnWindows>])
[RunIf<OnWindows>]
[DependsOn<RestoreModule>]
public class WindowsBuildModule : Module<string> { ... }
// Aggregates results — runs on any available worker
[DependsOn<LinuxBuildModule>]
[DependsOn<WindowsBuildModule>]
public class PublishModule : Module<string> { ... }
In this pipeline:
RestoreModuleis enqueued and any available worker (including the master) picks it up.- Once restore completes,
LinuxBuildModuleis enqueued for a Linux worker andWindowsBuildModulefor a Windows worker. These run in parallel on different machines. - Once both builds complete,
PublishModuleis enqueued and any available worker picks it up.