Reference · checked against source

API reference

Find a signature after you understand the model. Every public type in Wyrd and Wyrd.Testing is indexed here, with source links and a short example.

The build checks these declaration excerpts against the repository, verifies coverage of public types, every fluent and pipe builder member, every runtime member and all analyzer rules. Examples use application types from the Shop or show a small model to adapt; they are excerpts, rather than complete programs. Generic constraints and implementation details remain in the linked source.

Start with the philosophy, behavior and the read-model guide. Every host implements the same contracts: in memory, and durable on EF Core (see Running durable).

All. Open an entry for its declarations and example.

ICommandAn intention sent to exactly one owning slice.

View source · src/Wyrd/Messages/Messages.cs

public interface ICommand;
public sealed record CancelOrder(string OrderId) : ICommand;
IEventThe common marker accepted as a process trigger or projection fact.

View source · src/Wyrd/Messages/Messages.cs

public interface IEvent;
Process.StartOn<OrderPlaced>(e => e.OrderId);
IDomainEventAn internal accepted fact produced by a slice.

View source · src/Wyrd/Messages/Messages.cs

public interface IDomainEvent : IEvent;
public sealed record OrderCancelled(string OrderId) : IDomainEvent;
IIntegrationEventA public fact that can cross a context boundary.

View source · src/Wyrd/Messages/Messages.cs

public interface IIntegrationEvent : IEvent;
public sealed record PaymentConfirmed(string OrderId) : IIntegrationEvent;
IntegrationEventAttributeGives an integration contract its stable name and positive version.

View source · src/Wyrd/Messages/Messages.cs

public sealed class IntegrationEventAttribute(string name) : Attribute

public string Name { get; } = name;

public int Version { get; set; } = 1;
[IntegrationEvent("payments.payment-charged", Version = 1)]
public sealed record PaymentChargedV1(string OrderId, decimal Amount, string Currency) : IIntegrationEvent;
PipelineCreates pipe fragments and appends them with the >> extension operator.

View source · src/Wyrd/Pipe/Pipeline.cs

public static class Pipeline
public static Fragment Step(Delegate action)

Creates a command fragment; its function accepts the trigger and optionally earlier events.

Step(Stock.Reserve).Retry(3.Times).Undo(Stock.Release)
public static Fragment OnCompensated(Delegate outcome)

Sets the single outcome command run after compensation; it insists like an undo.

OnCompensated(Orders.Cancel).Retry(10.Times)
public static Fragment Await<TEvent>(Func<TEvent, object> key)

Waits for a correlated event; declare a business deadline with Within.

Await<PaymentConfirmed>(e => e.OrderId).Within(15.Minutes)
public static ProcessBuilder<TTrigger> operator >>(ProcessBuilder<TTrigger> process, Fragment next)

Appends a fragment to a typed builder; both syntaxes produce the same Process.

Start.On<OrderPlaced>(e => e.OrderId) >> Step(Shipping.Ship).Retry(3.Times)
using Wyrd.Pipe;
using static Wyrd.Pipe.Pipeline;

Process flow = Start.On<OrderPlaced>(e => e.OrderId)
    >> Step(Stock.Reserve).Retry(3.Times).Undo(Stock.Release)
    >> Await<PaymentConfirmed>(e => e.OrderId).Within(15.Minutes)
    >> OnCompensated(Orders.Cancel).Retry(10.Times);
StartStarts a pipe process with the same typed trigger and correlation key as StartOn.

View source · src/Wyrd/Pipe/Pipeline.cs

public static class Start

public static ProcessBuilder<TEvent> On<TEvent>(Func<TEvent, object> key, [CallerMemberName] string name = "")
var builder = Start.On<OrderPlaced>(e => e.OrderId, "Checkout");
FragmentApplies the same retry, undo and timeout policies as the fluent builder to a pipe stage.

View source · src/Wyrd/Pipe/Pipeline.cs

public sealed class Fragment
public Fragment Undo(Delegate compensation, Attempts? insist = null, Backoff backoff = Backoff.Exponential)

Associates the last step with its reversal and optional insistence policy; an undo cannot disable retries.

Step(Stock.Reserve).Retry(3.Times).Undo(Stock.Release, 20.Times)
public Fragment OrCompensate()

Opts into compensation after forward retries are exhausted; the default is suspension.

Step(Shipping.Ship).Retry(3.Times).OrCompensate()
public Fragment NoRetry(string reason)

Records why the last forward step must not retry.

Step(Shipping.Ship).NoRetry("the carrier retries on its side")
public Fragment NoCompensation(string reason)

Records why a step followed by more steps has nothing to undo.

Step(Notifications.Send).Retry(3.Times).NoCompensation("a notification cannot be recalled")
public Fragment Retry(Attempts attempts, Backoff backoff = Backoff.Exponential)

Declares the number of retries and backoff of the last forward step or outcome.

Step(Stock.Reserve).Retry(3.Times, Backoff.Linear).Undo(Stock.Release)
public Fragment Within(TimeSpan timeout)

Sets an Await deadline whose expiration starts compensation.

Await<PaymentConfirmed>(e => e.OrderId).Within(15.Minutes)
var reserve = Step(Stock.Reserve).Retry(3.Times).Undo(Stock.Release);
var notify = Step(Notifications.Send).NoRetry("delivery is handled by its owner")
    .NoCompensation("a notification cannot be recalled");
ProcessStores the trigger, ordered nodes, compensation outcome and structural fingerprint as data.

View source · src/Wyrd/Processes/Process.cs

public sealed class Process : IEquatable<Process>

public string Name { get; }

public Type Trigger { get; }

public ImmutableArray<ProcessNode> Nodes { get; }

public CompensatedNode? Outcome { get; }

public StepRef? OnCompensated

public static ProcessBuilder<TEvent> StartOn<TEvent>(Func<TEvent, object> key, [CallerMemberName] string name = "")

public string Fingerprint

public bool Equals(Process? other)

public string ToMermaid()
Process flow = Process.StartOn<OrderPlaced>(e => e.OrderId, "Checkout")
    .Then(Shipping.Ship).NoRetry("the carrier retries on its side");
string diagram = flow.ToMermaid();
string fingerprint = flow.Fingerprint;
ProcessBuilderBuilds an immutable fluent process; every modifier applies to the last node or outcome.

View source · src/Wyrd/Processes/ProcessBuilder.cs

public sealed class ProcessBuilder<TTrigger>
public ProcessBuilder<TTrigger> Then<TCommand>(Func<TTrigger, TCommand> step)

Appends a pure command step; the Delegate overload can read earlier event types after the trigger.

.Then(Shipping.Ship).Retry(3.Times)
public ProcessBuilder<TTrigger> Then(Delegate step)

Appends a pure command step; the Delegate overload can read earlier event types after the trigger.

.Then(Shipping.Ship).Retry(3.Times)
public ProcessBuilder<TTrigger> Await<TEvent>(Func<TEvent, object> key)

Waits for a correlated event; declare a business deadline with Within.

.Await<PaymentConfirmed>(e => e.OrderId).Within(15.Minutes)
public ProcessBuilder<TTrigger> OnCompensated<TCommand>(Func<TTrigger, TCommand> outcome)

Sets the single outcome command run after compensation; it insists like an undo.

.OnCompensated(Orders.Cancel).Retry(10.Times)
public ProcessBuilder<TTrigger> Undo<TCommand>(Func<TTrigger, TCommand> compensation, Attempts? insist = null, Backoff backoff = Backoff.Exponential)

Associates the last step with its reversal and optional insistence policy; an undo cannot disable retries.

.Then(Stock.Reserve).Retry(3.Times).Undo(Stock.Release, 20.Times)
public ProcessBuilder<TTrigger> Undo(Delegate compensation, Attempts? insist = null, Backoff backoff = Backoff.Exponential)

Associates the last step with its reversal and optional insistence policy; an undo cannot disable retries.

.Then(Stock.Reserve).Retry(3.Times).Undo(Stock.Release, 20.Times)
public ProcessBuilder<TTrigger> OrCompensate()

Opts into compensation after forward retries are exhausted; the default is suspension.

.Then(Shipping.Ship).Retry(3.Times).OrCompensate()
public ProcessBuilder<TTrigger> NoRetry(string reason)

Records why the last forward step must not retry.

.Then(Shipping.Ship).NoRetry("the carrier retries on its side")
public ProcessBuilder<TTrigger> NoCompensation(string reason)

Records why a step followed by more steps has nothing to undo.

.Then(Notifications.Send).Retry(3.Times).NoCompensation("a notification cannot be recalled")
public ProcessBuilder<TTrigger> Retry(Attempts attempts, Backoff backoff = Backoff.Exponential)

Declares the number of retries and backoff of the last forward step or outcome.

.Then(Stock.Reserve).Retry(3.Times, Backoff.Linear).Undo(Stock.Release)
public ProcessBuilder<TTrigger> Within(TimeSpan timeout)

Sets an Await deadline whose expiration starts compensation.

.Await<PaymentConfirmed>(e => e.OrderId).Within(15.Minutes)
public Process ToProcess()

Materializes the immutable model; implicit conversion to Process does the same.

Process flow = builder.ToProcess();
public static implicit operator Process(ProcessBuilder<TTrigger> builder)

Implicitly materializes a builder as its process model.

Process flow = builder;
Process flow = Process.StartOn<OrderPlaced>(e => e.OrderId)
    .Then(Stock.Reserve).Retry(3.Times).Undo(Stock.Release, 20.Times)
    .Then(Payments.Charge).Retry(3.Times, Backoff.Exponential).OrCompensate().Undo(Payments.Refund)
    .Await<PaymentConfirmed>(e => e.OrderId).Within(15.Minutes)
    .Then(Shipping.Ship).NoRetry("the carrier retries on its side")
    .OnCompensated(Orders.Cancel).Retry(10.Times);
BackoffSelects immediate, linear or exponential retry delays, capped at one minute.

View source · src/Wyrd/Processes/ProcessNode.cs

public enum Backoff
var retry = new RetryPolicy(3.Times, Backoff.Linear);
RetryPolicyDescribes retries and calculates their delay; Undo supplies the default insistence policy.

View source · src/Wyrd/Processes/ProcessNode.cs

public sealed record RetryPolicy(Attempts Attempts, Backoff Backoff)

public static RetryPolicy Undo { get; } = new(new Attempts(10), Backoff.Exponential);

public TimeSpan DelayBefore(int retry)
TimeSpan firstDelay = RetryPolicy.Undo.DelayBefore(1);
StepRefNames a pure command-producing function and lists any earlier event types it reads.

View source · src/Wyrd/Processes/ProcessNode.cs

public sealed record StepRef(string Name)

public IReadOnlyList<Type> Reads { get; init; } = [];

public static StepRef From(Delegate step)

public bool Equals(StepRef? other)
StepRef step = StepRef.From(Stock.Reserve);
string name = step.Name;
ProcessNodeThe common record for a command step, wait or compensation outcome.

View source · src/Wyrd/Processes/ProcessNode.cs

public abstract record ProcessNode;
foreach (ProcessNode node in flow.Nodes) Console.WriteLine(node);
StepNodeHolds a step, its undo, explicit policy reasons and behavior after retries run out.

View source · src/Wyrd/Processes/ProcessNode.cs

public sealed record StepNode(StepRef Action, StepRef? Compensation = null, RetryPolicy? Retry = null) : ProcessNode

public string? NoCompensationReason { get; init; }

public string? NoRetryReason { get; init; }

public bool CompensateWhenExhausted { get; init; }

public RetryPolicy? CompensationRetry { get; init; }

public RetryPolicy UndoPolicy
foreach (var step in flow.Nodes.OfType<StepNode>())
    Console.WriteLine(step.UndoPolicy.Attempts.Count);
CompensatedNodeStores the command sent after reverse compensation and its insistence policy.

View source · src/Wyrd/Processes/ProcessNode.cs

public sealed record CompensatedNode(StepRef Action, RetryPolicy? Retry = null) : ProcessNode

public RetryPolicy Policy
RetryPolicy? policy = flow.Outcome?.Policy;
AwaitNodeStores the awaited event type and optional declared timeout.

View source · src/Wyrd/Processes/ProcessNode.cs

public sealed record AwaitNode(Type Event, TimeSpan? Timeout = null) : ProcessNode

public bool Equals(AwaitNode? other)
var waits = flow.Nodes.OfType<AwaitNode>().Select(wait => wait.Timeout);
ResultRepresents success or a business error through implicit conversions and Match.

View source · src/Wyrd/Result.cs

public readonly record struct Result<TOk, TError>

public bool IsOk { get; }

public TResult Match<TResult>(Func<TOk, TResult> ok, Func<TError, TResult> error)

public static implicit operator Result<TOk, TError>(TOk value)

public static implicit operator Result<TOk, TError>(TError error)
Result<PaymentCharged, PaymentError> result = new PaymentCharged("A-1", 89m);
var accepted = result.IsOk;
var text = result.Match(e => e.OrderId, error => error.ToString());
InMemoryRuntimeHosts the deterministic engine with JSON boundaries, an in-memory store and a virtual clock: tests, development, simulation and replay. Same builder and runtime contract as the durable EF Core host.

View source · src/Wyrd/Runtime/InMemoryRuntime.cs

public sealed class InMemoryRuntime : IWyrdBuilder<InMemoryRuntime>, IWyrdRuntime, IDisposable
public InMemoryRuntime()

Constructs the host; the parameterless reflection entry point is for development, while the JSON-context overload supports Native AOT.

using var shop = new InMemoryRuntime(ShopJson.Default);
public InMemoryRuntime(JsonSerializerContext json)

Constructs the host; the parameterless reflection entry point is for development, while the JSON-context overload supports Native AOT.

using var shop = new InMemoryRuntime(ShopJson.Default);
public InMemoryRuntime UseRetention(Retention retention)

How long this runtime keeps what it handled, on its virtual clock. Nothing expires by default; expired data goes when the clock moves.

var shop = ShopModule.CreateRuntime().UseRetention(new Retention
{
    Deduplication = Keep.For(1.Days),
    FinishedInstances = Keep.For(1.Days),
    RelayedMessages = Keep.For(1.Days),
    Events = Keep.For(1.Days),
    DeadLetters = Keep.For(1.Days),
});
public InMemoryRuntime UseHistoryArchive(IHistoryArchive archive)

Hands the histories of finished instances to an archive before retention removes them; an instance is removed only once its history is archived.

shop.UseHistoryArchive(new BlobArchive(container));
public int JournalLimit

The journal keeps at least this many recent entries (10,000 by default), so a long run stays bounded.

shop.JournalLimit = 1_000;
public TimeSpan Now

Reads elapsed virtual time from the host epoch.

TimeSpan elapsed = shop.Now;
public IReadOnlyList<JournalEntry> Journal

Returns a snapshot of readable observations.

foreach (var entry in shop.Journal) Console.WriteLine(entry);
public IReadOnlyList<IDomainEvent> History

Returns the domain events produced by all slices; this differs from one process boundary history.

var acceptedFacts = shop.History;
public IReadOnlyList<IIntegrationEvent> Published

Returns the integration events mapped by publications.

var publicFacts = shop.Published;
public InMemoryRuntime AddIntegration<TCommand, TEvent, TError>(Integration<TCommand, TEvent, TError> integration)

Registers the integration that owns a command carried out by another system: the only I/O in Wyrd.

shop.AddIntegration(new PaymentGateway(http));
public InMemoryRuntime AddSlice<TCommand, TState, TEvent, TError>(Slice<TCommand, TState, TEvent, TError> slice)

Registers the authoritative command owner before handling messages.

shop.AddSlice(new ChargePaymentSlice());
public InMemoryRuntime AddPublication<TDomainEvent, TIntegrationEvent>(Publication<TDomainEvent, TIntegrationEvent> publication)

Registers the domain-to-contract mapping.

shop.AddPublication(new PaymentChargedPublication());
public InMemoryRuntime AddUpcaster<TFrom, TTo>(Upcaster<TFrom, TTo> upcaster)

Registers a mapping to the contract version a subscriber understands.

shop.AddUpcaster(new ChargedUpcaster());
public InMemoryRuntime AddProjection<TView>(Projection<TView> projection)

Registers pure view handlers and their serialized view type.

shop.AddProjection(new OrderSummaries());
public InMemoryRuntime AddProcess(Process process)

Registers and validates a process definition before delivery.

shop.AddProcess(Processes.OrderFulfillment);
public async Task<Decision> SendAsync(ICommand command, CancellationToken cancellationToken = default)

Sends a command, drains its queued consequences and returns its accepted or rejected decision.

Decision outcome = await shop.SendAsync(new ChargePayment("A-1", 89m));
public async Task PublishAsync(IIntegrationEvent @event, CancellationToken cancellationToken = default)

Delivers an external integration event; domain events cannot be published through this API.

await shop.PublishAsync(new PaymentConfirmed("A-1"));
public async Task AdvanceAsync(TimeSpan by, CancellationToken cancellationToken = default)

Moves the virtual clock and fires due retries and timeouts.

await shop.AdvanceAsync(15.Minutes);
public Task ResumeAsync(Process process, object key, CancellationToken cancellationToken = default)

Resends a suspended instance's pending action with the same message ID; a resumed wait starts its deadline again.

await shop.ResumeAsync(Processes.OrderFulfillment, "A-1");
public Task CompensateAsync(Process process, object key, CancellationToken cancellationToken = default)

Requests compensation of a forward-suspended instance and fences its uncertain pending step.

await shop.CompensateAsync(Processes.OrderFulfillment, "A-1");
public InstanceHistory HistoryOf(Process process, object key)

Returns one existing process instance's recorded boundary history; an unknown instance throws.

InstanceHistory history = shop.HistoryOf(Processes.OrderFulfillment, "A-1");
public string? ReasonOf(Process process, object key)

Returns the instance's suspension reason, or null.

string? reason = shop.ReasonOf(Processes.OrderFulfillment, "A-1");
public void FailNext<TCommand>(int times = 1)

Injects transient failures for the next matching command deliveries in this in-memory host.

shop.FailNext<ChargePayment>(2);
public ProcessStatus? StatusOf(Process process, object key)

Returns the process instance status, or null if it does not exist.

ProcessStatus? status = shop.StatusOf(Processes.OrderFulfillment, "A-1");
public TState StateOf<TState>(object key)

Returns the keyed domain snapshot, or that state's Initial value when absent.

Payment state = shop.StateOf<Payment>("A-1");
public TView? View<TView>(object key)

Returns one stored view, or null for a missing key.

OrderSummary? row = shop.View<OrderSummary>("A-1");
public IReadOnlyList<TView> Views<TView>()

Returns stored rows in creation order; apply an explicit display sort when needed.

var rows = shop.Views<OrderSummary>();
public async Task CatchUpAsync(CancellationToken cancellationToken = default)

Seeds projections added to a runtime that already has states from those states. The runtime does it on its own when idle; call it to have the views before the next message.

shop.AddProjection(new StockLevels());
await shop.CatchUpAsync();
public void Dispose()

Disposes the host's synchronization gate.

using var shop = ShopModule.CreateRuntime();
public InMemoryRuntime UseTransport(IWyrdTransport transport)

Sends the integration events this runtime publishes to other services through a transport.

var transport = new InMemoryTransport();
var payments = PaymentsModule.CreateRuntime().UseTransport(transport);
public IReadOnlyList<string> Subscriptions

The integration-event contracts from other services this runtime uses: what a transport subscribes to.

foreach (var contract in shop.Subscriptions) Console.WriteLine(contract);
public async Task ReceiveAsync(TransportMessage message, CancellationToken cancellationToken = default)

Delivers an integration event another service published, keeping its message id: a redelivery is handled once.

await shipping.ReceiveAsync(message);
using var shop = ShopModule.CreateRuntime();
await shop.SendAsync(new PlaceOrder("A-1", "KEYBOARD", 1, 89m));
await shop.PublishAsync(new PaymentConfirmed("A-1"));
await shop.AdvanceAsync(15.Minutes);
var order = shop.View<OrderSummary>("A-1");
HistoryKindIdentifies starts, sends, replies, received facts, timeouts, operator actions and final states.

View source · src/Wyrd/Runtime/InstanceHistory.cs

public enum HistoryKind
var commands = history.Entries.Where(e => e.Kind == HistoryKind.Sent);
HistoryEntryRecords one boundary observation with its stored payload, step, attempt and outcome or reason.

View source · src/Wyrd/Runtime/InstanceHistory.cs

public sealed record HistoryEntry(DateTimeOffset At, HistoryKind Kind)

public string? Type { get; init; }

public string? Payload { get; init; }

public int? Step { get; init; }

public bool Compensating { get; init; }

public int? Attempt { get; init; }

public string? Outcome { get; init; }

public string? Reason { get; init; }
foreach (HistoryEntry entry in history.Entries)
    Console.WriteLine($"{entry.Kind}: {entry.Type}");
InstanceHistoryExports an ordered process-instance history with its key and structural fingerprint.

View source · src/Wyrd/Runtime/InstanceHistory.cs

public sealed record InstanceHistory(string Process, string Key, string Fingerprint, IReadOnlyList<HistoryEntry> Entries)

public string ToJson()

public static InstanceHistory FromJson(string json)
var history = shop.HistoryOf(Processes.OrderFulfillment, "A-1");
string json = history.ToJson();
var restored = InstanceHistory.FromJson(json);
JournalEntryA human-readable observation timestamped using the virtual clock.

View source · src/Wyrd/Runtime/JournalEntry.cs

public sealed record JournalEntry(TimeSpan At, string Process, object? Key, string Message)
foreach (JournalEntry entry in shop.Journal) Console.WriteLine(entry);
ProcessStatusDescribes an instance as Running, Awaiting, Completed, Compensated or Suspended.

View source · src/Wyrd/Runtime/ProcessStatus.cs

public enum ProcessStatus
if (shop.StatusOf(Processes.OrderFulfillment, "A-1") == ProcessStatus.Suspended)
    Console.WriteLine(shop.ReasonOf(Processes.OrderFulfillment, "A-1"));
IStateThe nongeneric marker identifies state; implement the generic contract in application code.

View source · src/Wyrd/Slices/IState.cs

public interface IState
public abstract record Order : IState<Order> { /* Initial and Evolve */ }
IState<TSelf>Provides the initial snapshot and a pure transition from each accepted domain event.

View source · src/Wyrd/Slices/IState.cs

public interface IState<TSelf> : IState

static abstract TSelf Initial { get; }

TSelf Evolve(IDomainEvent domainEvent);
public static Order Initial { get; } = new None();
public Order Evolve(IDomainEvent e) => e is OrderCancelled ? new Cancelled() : this;
ProjectionRegisters pure event-to-view updates; a null result deletes the selected view.

View source · src/Wyrd/Slices/Projection.cs

public abstract class Projection<TView> : IProjectionHandler

protected void On<TEvent>(Func<TEvent, object> key, Func<TView?, TEvent, TView?> apply)
public sealed class OpenOrders : Projection<OrderSummary>
{
    public OpenOrders()
    {
        On<OrderPlaced>(e => e.OrderId, (_, e) => new(e.OrderId, e.Sku, e.Quantity, e.Amount, "Placed"));
        On<OrderCancelled>(e => e.OrderId, (_, _) => null);
    }
}
PublicationMaps an internal domain fact to an explicit public contract.

View source · src/Wyrd/Slices/Publication.cs

public abstract class Publication<TDomainEvent, TIntegrationEvent> : IPublicationHandler

public abstract TIntegrationEvent Map(TDomainEvent domainEvent);
public sealed class ChargedPublication : Publication<PaymentCharged, PaymentChargedV1>
{
    public override PaymentChargedV1 Map(PaymentCharged e) => new(e.OrderId, e.Amount, "EUR");
}
SliceOwns a command and decides one event or rejection from its keyed state.

View source · src/Wyrd/Slices/Slice.cs

public abstract class Slice<TCommand, TState, TEvent, TError> : ISliceHandler

public abstract object KeyOf(TCommand command);

public abstract Result<TEvent, TError> Decide(TCommand command, TState state);
public override object KeyOf(ChargePayment command) => command.OrderId;
public override Result<PaymentCharged, PaymentError> Decide(ChargePayment command, Payment state) =>
    state is Payment.None ? new PaymentCharged(command.OrderId, command.Amount) : PaymentError.AlreadyCharged;
DecisionThe runtime returns an accepted domain event or a rejected business error.

View source · src/Wyrd/Slices/Slice.cs

public abstract record Decision

public sealed record Accepted(IDomainEvent Event) : Decision;

public sealed record Rejected(object Error) : Decision;
var decision = await shop.SendAsync(new CancelOrder("A-1"));
if (decision is Decision.Rejected rejected) Console.WriteLine(rejected.Error);
UpcasterMaps an older integration contract into the newer version a subscriber consumes.

View source · src/Wyrd/Slices/Upcaster.cs

public abstract class Upcaster<TFrom, TTo> : IUpcasterHandler

public abstract TTo Map(TFrom previous);
public sealed class ChargedUpcaster : Upcaster<PaymentChargedV1, PaymentChargedV2>
{
    public override PaymentChargedV2 Map(PaymentChargedV1 e) => new(e.OrderId, e.Amount, e.Currency);
}
AttemptsA positive retry count expressed with the C# extension literal Times.

View source · src/Wyrd/Time.cs

public readonly record struct Attempts

public Attempts(int count)

public int Count { get; }
Attempts attempts = 3.Times;
int count = attempts.Count;
LiteralsAdds readable TimeSpan and Attempts properties to integer expressions.

View source · src/Wyrd/Time.cs

public static class Literals

public TimeSpan Seconds

public TimeSpan Minutes

public TimeSpan Hours

public TimeSpan Days

public Attempts Times
TimeSpan deadline = 15.Minutes;
TimeSpan settle = 1.Days;
Attempts retries = 3.Times;
UnionAttributeMarks an abstract record whose nested sealed records are its closed cases.

View source · src/Wyrd/UnionAttribute.cs

public sealed class UnionAttribute : Attribute;
[Union]
public abstract record Payment
{
    public sealed record None : Payment;
    public sealed record Charged(decimal Amount) : Payment;
}
WyrdJsonContextAttributeMarks the source-generated application JSON context checked by the serialization analyzers.

View source · src/Wyrd/WyrdJsonContextAttribute.cs

public sealed class WyrdJsonContextAttribute : Attribute;
[WyrdJsonContext]
[JsonSerializable(typeof(ChargePayment))]
[JsonSerializable(typeof(Payment))]
public partial class ShopJson : JsonSerializerContext;

// Register every reachable message, state, error, view and union case.
var shop = new InMemoryRuntime(ShopJson.Default);
InvariantChecks the settled runtime, returning null for success or a useful failure explanation.

View source · src/Wyrd.Testing/Invariant.cs

public sealed record Invariant(string Name, Func<InMemoryRuntime, string?> Check)

public static Invariant ProcessesSettle { get; }

public static Invariant NothingNeedsAttention { get; }

public static IReadOnlyList<Invariant> Defaults { get; } = [ProcessesSettle, NothingNeedsAttention];
Invariant conserved = new("stock is nonnegative", shop =>
    shop.StateOf<StockItem>("KEYBOARD").Available >= 0 ? null : "negative stock");
Invariant[] checks = [.. Invariant.Defaults, conserved];
ProjectionScenarioApplies facts in the supplied order and compares a view as stored JSON.

View source · src/Wyrd.Testing/ProjectionScenario.cs

public sealed class ProjectionScenario<TView>

public ProjectionScenario<TView> Given(params IEvent[] events)

public ProjectionScenario<TView> Then(object key, TView? expected)
Scenario.For(new OrderSummaries())
    .Given(new OrderPlaced("A-1", "KEYBOARD", 1, 89m))
    .Then("A-1", new OrderSummary("A-1", "KEYBOARD", 1, 89m, "Placed"));
ReplayReportExposes recorded and replayed entries, the first divergence, structural reason and replay error.

View source · src/Wyrd.Testing/ReplayReport.cs

public sealed class ReplayReport

public InstanceHistory Recorded { get; }

public IReadOnlyList<HistoryEntry> Replayed { get; }

public IReadOnlyList<JournalEntry> Journal { get; }

public ReplayDivergence? Divergence { get; }

public string? Reason { get; init; }

public Exception? Error { get; init; }

public bool Diverged

public void ThrowIfDiverged()
report.ThrowIfDiverged();
if (report.Divergence is { } first) Console.WriteLine(first.Index);
ReplayDivergencePoints to the first unequal entry; a null side means that entry was absent.

View source · src/Wyrd.Testing/ReplayReport.cs

public sealed record ReplayDivergence(int Index, HistoryEntry? Recorded, HistoryEntry? Replayed);
HistoryEntry? expected = report.Divergence?.Recorded;
HistoryEntry? actual = report.Divergence?.Replayed;
ScenarioStarts a JSON-checked business specification for a slice or projection.

View source · src/Wyrd.Testing/Scenario.cs

public static class Scenario

public static SliceScenario<TCommand, TState, TEvent, TError> For<TCommand, TState, TEvent, TError>(
Slice<TCommand, TState, TEvent, TError> slice)

public static ProjectionScenario<TView> For<TView>(Projection<TView> projection)
Scenario.For(new ChargePaymentSlice())
    .Given(new PaymentCharged("A-1", 89m))
    .When(new ChargePayment("A-1", 89m))
    .ThenRejected(PaymentError.AlreadyCharged);
SimulationOptionsConfigures runs, seed, hostile probabilities, settling time and bounded greedy shrinking.

View source · src/Wyrd.Testing/SimulationOptions.cs

public sealed record SimulationOptions

public int Runs { get; init; } = 200;

public int Seed { get; init; } = 1;

public double FaultRate { get; init; } = 0.1;

public double DuplicateRate { get; init; } = 0.1;

public double ReorderRate { get; init; } = 0.3;

public TimeSpan SettleFor { get; init; } = TimeSpan.FromDays(1);

public bool Shrink { get; init; } = true;

public int ShrinkBudget { get; init; } = 200;

public SimulationOptions Only(int seed)
var options = new SimulationOptions { Runs = 200, Shrink = true, ShrinkBudget = 200 };
var replay = options.Only(9);
ViolationDescribes a failing seed, invariant, detail and journal, optionally with its reduced future.

View source · src/Wyrd.Testing/SimulationReport.cs

public sealed record Violation(int Seed, string Invariant, string Detail, IReadOnlyList<JournalEntry> Journal)

public ShrunkFuture? Shrunk { get; init; }
foreach (Violation failure in report.Violations)
    Console.WriteLine($"{failure.Invariant}: seed {failure.Seed}");
ShrunkFutureContains a smaller failure found within budget, without promising a global minimum.

View source · src/Wyrd.Testing/SimulationReport.cs

public sealed record ShrunkFuture(
string Detail, IReadOnlyList<string> Perturbations, IReadOnlyList<JournalEntry> Journal, int Replays);
var smallest = report.Violations.First().Shrunk;
var perturbations = smallest?.Perturbations;
SimulationReportExposes explored runs, options and violations and can fail a test with the first case per invariant.

View source · src/Wyrd.Testing/SimulationReport.cs

public sealed class SimulationReport(int runs, SimulationOptions options, IReadOnlyList<Violation> violations)

public int Runs { get; } = runs;

public SimulationOptions Options { get; } = options;

public IReadOnlyList<Violation> Violations { get; } = violations;

public bool Passed

public void ThrowIfViolated()
report.ThrowIfViolated();
bool passed = report.Passed;
var failures = report.Violations;
SkuldExplores seeded futures with faults, redelivery and reordering against explicit invariants.

View source · src/Wyrd.Testing/Skuld.cs

public static class Skuld

public static async Task<SimulationReport> SimulateAsync(
Func<InMemoryRuntime> system,
Func<InMemoryRuntime, Task> scenario,
IReadOnlyList<Invariant> invariants,
SimulationOptions? options = null)
var report = await Skuld.SimulateAsync(ShopModule.CreateRuntime,
    shop => shop.SendAsync(new PlaceOrder("A-1", "KEYBOARD", 1, 89m)),
    Invariant.Defaults, new SimulationOptions { Runs = 200 });
report.ThrowIfViolated();
SliceScenarioSupplies historical facts or an explicit state, then asks the slice to decide a command.

View source · src/Wyrd.Testing/SliceScenario.cs

public sealed class SliceScenario<TCommand, TState, TEvent, TError>

public SliceScenario<TCommand, TState, TEvent, TError> Given(params IDomainEvent[] history)

public SliceScenario<TCommand, TState, TEvent, TError> GivenState(TState state)

public SliceOutcome<TCommand, TState, TEvent, TError> When(TCommand command)
Scenario.For(new ChargePaymentSlice())
    .GivenState(new Payment.None())
    .When(new ChargePayment("A-1", 89m))
    .Then(new PaymentCharged("A-1", 89m));
SliceOutcomeAsserts the accepted event, business rejection or evolved state after a command.

View source · src/Wyrd.Testing/SliceScenario.cs

public sealed class SliceOutcome<TCommand, TState, TEvent, TError>

public SliceOutcome<TCommand, TState, TEvent, TError> Then(TEvent expected)

public SliceOutcome<TCommand, TState, TEvent, TError> ThenRejected(TError expected)

public SliceOutcome<TCommand, TState, TEvent, TError> ThenState(TState expected)
Scenario.For(new ChargePaymentSlice())
    .When(new ChargePayment("A-1", 89m))
    .Then(new PaymentCharged("A-1", 89m))
    .ThenState(new Payment.Charged(89m));
UrdRuns the process against recorded replies and virtual time, checking the first difference.

View source · src/Wyrd.Testing/Urd.cs

public static class Urd

public static async Task<ReplayReport>
    ReplayAsync(Func<InMemoryRuntime> system,
        Process process, InstanceHistory history)
var report = await Urd.ReplayAsync(ShopModule.CreateRuntime,
    Processes.OrderFulfillment, history);
report.ThrowIfDiverged();
WyrdAssertionExceptionSignals a failed scenario, invariant or replay expectation to the test runner.

View source · src/Wyrd.Testing/WyrdAssertionException.cs

public sealed class WyrdAssertionException : Exception

public WyrdAssertionException()

public WyrdAssertionException(string message)

public WyrdAssertionException(string message, Exception innerException)
report.ThrowIfViolated(); // throws WyrdAssertionException if any invariant failed
IntegrationThe only place Wyrd code performs I/O: the single owner of a command carried out by another system. Its event is recorded like a domain event, its error is a business rejection, and an exception is a transient failure for the process retry policy.

View source · src/Wyrd/Integrations/Integration.cs

public abstract class Integration<TCommand, TEvent, TError> : IIntegrationHandler

public abstract Task<Result<TEvent, TError>> ExecuteAsync(TCommand command, IntegrationContext context, CancellationToken cancellationToken);

public virtual object KeyOf(TCommand command) => typeof(TCommand).Name;
public sealed class PaymentGateway(HttpClient http) : Integration<ChargeCard, CardCharged, PaymentError>
{
    public override async Task<Result<CardCharged, PaymentError>> ExecuteAsync(
        ChargeCard command, IntegrationContext context, CancellationToken cancellationToken)
    {
        // Pass context.IdempotencyKey to the provider: a retry never charges twice.
        ...
    }
}
IntegrationContextWhat an integration knows about the call it makes: the idempotency key (the command message id, the same on every retry and redelivery) and the attempt number.

View source · src/Wyrd/Integrations/Integration.cs

public sealed record IntegrationContext(Guid IdempotencyKey, int Attempt);
request.Headers.Add("Idempotency-Key", context.IdempotencyKey.ToString());
IWyrdBuilderWhat every host accepts: register a module once and run it in memory or on a durable host.

View source · src/Wyrd/Runtime/IWyrdBuilder.cs

public interface IWyrdBuilder<out TSelf>

TSelf AddSlice<TCommand, TState, TEvent, TError>(Slice<TCommand, TState, TEvent, TError> slice)

TSelf AddIntegration<TCommand, TEvent, TError>(Integration<TCommand, TEvent, TError> integration)

TSelf AddPublication<TDomainEvent, TIntegrationEvent>(Publication<TDomainEvent, TIntegrationEvent> publication)

TSelf AddUpcaster<TFrom, TTo>(Upcaster<TFrom, TTo> upcaster)

TSelf AddProjection<TView>(Projection<TView> projection)

TSelf AddProcess(Process process);
public static TRuntime Register<TRuntime>(TRuntime runtime) where TRuntime : IWyrdBuilder<TRuntime> =>
    runtime.AddSlice(new ChargePaymentSlice()).AddProcess(Processes.OrderFulfillment);
IWyrdRuntimeWhat application code needs from Wyrd whatever the host: send commands, deliver facts, receive events from other services, read states and views, and operate suspended instances.

View source · src/Wyrd/Runtime/IWyrdRuntime.cs

public interface IWyrdRuntime

Task<Decision> SendAsync(ICommand command, CancellationToken cancellationToken = default);

Task PublishAsync(IIntegrationEvent integrationEvent, CancellationToken cancellationToken = default);

Task ReceiveAsync(TransportMessage message, CancellationToken cancellationToken = default);

IReadOnlyList<string> Subscriptions { get; }

Task<ProcessStatus?> StatusOfAsync(Process process, object key, CancellationToken cancellationToken = default);

Task ResumeAsync(Process process, object key, CancellationToken cancellationToken = default);

Task CompensateAsync(Process process, object key, CancellationToken cancellationToken = default);

Task<InstanceHistory?> HistoryOfAsync(Process process, object key, CancellationToken cancellationToken = default);

Task<TState> StateOfAsync<TState>(object key, CancellationToken cancellationToken = default)

Task<TView?> ViewAsync<TView>(object key, CancellationToken cancellationToken = default)

Task<IReadOnlyList<TView>> ViewsAsync<TView>(CancellationToken cancellationToken = default)
app.MapPost("/orders", async (PlaceOrderRequest request, IWyrdRuntime wyrd) =>
    await wyrd.SendAsync(request.ToCommand()) switch
    {
        Decision.Accepted => Results.Accepted(),
        Decision.Rejected rejected => Results.Problem(rejected.Error.ToString(), statusCode: 409),
    });
IHistoryArchiveWhere the histories of finished instances go before retention removes them. At least once: store each history by process, key and first entry, overwriting; an exception keeps the batch for the next pass.

View source · src/Wyrd/Runtime/IHistoryArchive.cs

public interface IHistoryArchive

Task ArchiveAsync(IReadOnlyList<InstanceHistory> histories, CancellationToken cancellationToken);
public sealed class BlobArchive(BlobContainerClient container) : IHistoryArchive
{
    public async Task ArchiveAsync(IReadOnlyList<InstanceHistory> histories, CancellationToken cancellationToken)
    {
        foreach (var history in histories)
            await container.GetBlobClient($"{history.Process}/{history.Key}/{history.Entries[0].At:O}.json")
                .UploadAsync(BinaryData.FromString(history.ToJson()), overwrite: true, cancellationToken);
    }
}
RetentionHow long a host keeps what it handled. On a durable host every member is required: retention is a decision written in the application, never a default.

View source · src/Wyrd/Runtime/Retention.cs

public sealed record Retention

public required Keep Deduplication { get; init; }

public required Keep FinishedInstances { get; init; }

public required Keep RelayedMessages { get; init; }

public required Keep Events { get; init; }

public required Keep DeadLetters { get; init; }

public static Retention InMemory { get; } = new()
new Retention
{
    Deduplication = Keep.For(7.Days),
    FinishedInstances = Keep.For(30.Days),
    RelayedMessages = Keep.For(7.Days),
    Events = Keep.Forever("the shop's record of what happened"),
    DeadLetters = Keep.Forever("an operator reviews each one"),
}
KeepOne retention decision: keep something for a period, or forever with the reason written down.

View source · src/Wyrd/Runtime/Retention.cs

public sealed record Keep

public static Keep For(TimeSpan period)

public static Keep Forever(string why)

public TimeSpan? Period { get; }

public string? Reason { get; }
Deduplication = Keep.For(7.Days),
Events = Keep.Forever("audited by finance")
TransportMessageAn integration event on its way between services: its contract, its JSON payload, the id it keeps end to end, and the trace context it was published in.

View source · src/Wyrd/Runtime/Transport.cs

public sealed record TransportMessage(Guid MessageId, string Type, string Payload)

public string? TraceParent { get; init; }

public string? TraceState { get; init; }
var message = new TransportMessage(id, "shipping.order-shipped v1", payload) { TraceParent = traceParent };
IWyrdTransportCarries published integration events to other services, at least once (Wyrd.RabbitMq, or InMemoryTransport in tests). Only integration events cross service boundaries.

View source · src/Wyrd/Runtime/Transport.cs

public interface IWyrdTransport

Task SendAsync(TransportMessage message, CancellationToken cancellationToken = default);
var runtime = ShopModule.CreateRuntime().UseTransport(await RabbitMqTransport.CreateAsync(connection));
InMemoryTransportConnects runtimes in one process deterministically: every message reaches every subscriber, in order, before SendAsync returns.

View source · src/Wyrd/Runtime/Transport.cs

public sealed class InMemoryTransport : IWyrdTransport

public InMemoryTransport Subscribe(IWyrdRuntime runtime)

public IReadOnlyList<TransportMessage> Sent

public async Task SendAsync(TransportMessage message, CancellationToken cancellationToken = default)
var transport = new InMemoryTransport();
var payments = PaymentsModule.CreateRuntime().UseTransport(transport);
transport.Subscribe(ShippingModule.CreateRuntime());
WyrdExceptionA failure a host or an application can handle: retry it, report it, dead-letter it or answer an operator. Derives from InvalidOperationException, so existing catch blocks still work.

View source · src/Wyrd/WyrdException.cs

public class WyrdException : InvalidOperationException

public WyrdException(string message)

public WyrdException(string message, Exception innerException)
catch (WyrdException ex) { logger.LogWarning(ex, "Wyrd could not handle the request"); }
ConcurrencyConflictExceptionAnother transaction changed the same state, instance or read model first. Retrying is always safe; hosts retry on their own, so you see it only when Wyrd joins your transaction.

View source · src/Wyrd/WyrdException.cs

public sealed class ConcurrencyConflictException : WyrdException
catch (ConcurrencyConflictException) { await transaction.RollbackAsync(); /* and retry */ }
UnknownMessageTypeExceptionA message names a type nothing registered here uses.

View source · src/Wyrd/WyrdException.cs

public sealed class UnknownMessageTypeException(string messageType)
catch (UnknownMessageTypeException ex) { logger.LogError("Unknown contract {Type}", ex.MessageType); }
MissingJsonMetadataExceptionA type has no JSON metadata in the application [WyrdJsonContext]: add [JsonSerializable(typeof(T))].

View source · src/Wyrd/WyrdException.cs

public sealed class MissingJsonMetadataException(Type type, Exception innerException)
[WyrdJsonContext]
[JsonSerializable(typeof(OrderSummary))]
public partial class ShopJson : JsonSerializerContext;
MissingUpcasterExceptionA subscriber expects a newer version of a contract than was published, and no upcaster bridges the gap.

View source · src/Wyrd/WyrdException.cs

public sealed class MissingUpcasterException(string message) : WyrdException(message);
runtime.AddUpcaster(new PaymentChargedV1ToV2());
NotAnIntegrationEventExceptionSomething other than an integration event was offered across a service boundary.

View source · src/Wyrd/WyrdException.cs

public sealed class NotAnIntegrationEventException(string messageType)
catch (NotAnIntegrationEventException ex) { /* reject the message: ex.MessageType */ }
InstanceStateExceptionAn operator asked a process instance for something it cannot do now: it does not exist, is not suspended, or was already compensating.

View source · src/Wyrd/WyrdException.cs

public sealed class InstanceStateException(string process, string key, string message) : WyrdException(message)
catch (InstanceStateException ex) { return Results.Conflict(ex.Message); }

Rules below are all currently declared diagnostics, not a continuous range of numbers. WYRD0010 was removed; WYRD0014 requires an explicit retry decision.

WYRD0001 · ErrorA message is either a command or an event.

No dedicated code fix.

Diagnostic declaration · WYRD0001

public sealed record ChargePayment(string OrderId) : ICommand;
public sealed record PaymentCharged(string OrderId) : IDomainEvent;
WYRD0002 · ErrorA command has exactly one owner.

No dedicated code fix.

Diagnostic declaration · WYRD0002

// Keep one Slice<ChargePayment, Payment, PaymentCharged, PaymentError>.
WYRD0003 · InfoName events in the past tense.

No dedicated code fix.

Diagnostic declaration · WYRD0003

public sealed record PaymentCharged(string OrderId) : IDomainEvent;
WYRD0004 · InfoName commands in the imperative.

No dedicated code fix.

Diagnostic declaration · WYRD0004

public sealed record ChargePayment(string OrderId) : ICommand;
WYRD0014 · WarningSay how a step retries.

No dedicated code fix.

Diagnostic declaration · WYRD0014

.Then(Shipping.Ship).NoRetry("the carrier retries on its side")
WYRD0011 · WarningSay how a step is undone.

No dedicated code fix.

Diagnostic declaration · WYRD0011

.Then(Stock.Reserve).Retry(3.Times).Undo(Stock.Release)
WYRD0012 · WarningA wait needs a timeout.

No dedicated code fix.

Diagnostic declaration · WYRD0012

.Await<PaymentConfirmed>(e => e.OrderId).Within(15.Minutes)
WYRD0020 · ErrorA switch over a union handles every case.

No dedicated code fix.

Diagnostic declaration · WYRD0020

// Handle Payment.None, Payment.Charged and Payment.Refunded explicitly.
WYRD0013 · WarningWrite processes in the repository's style.

Convert the whole process between fluent and pipe syntax; Fix all uses the Roslyn batch fixer.

Diagnostic declaration · WYRD0013

# .editorconfig
wyrd_process_style = fluent
WYRD0101 · ErrorIntegration events must not expose domain types.

No dedicated code fix.

Diagnostic declaration · WYRD0101

// Use primitives in the contract, not OrderLine or Money.
public sealed record PriceV1(decimal Amount, string Currency) : IIntegrationEvent;
WYRD0102 · ErrorIntegration events need a name, a version and IIntegrationEvent.

No dedicated code fix.

Diagnostic declaration · WYRD0102

[IntegrationEvent("payments.confirmed", Version = 1)]
public sealed record PaymentConfirmed(string OrderId) : IIntegrationEvent;
WYRD0300 · ErrorWyrd serializes this type: declare it in the JSON context.

Add JsonSerializable; Fix all completes declarations across a document, project or solution.

Diagnostic declaration · WYRD0300

[JsonSerializable(typeof(ChargePayment))]
WYRD0301 · ErrorUnion cases must be declared as JSON derived types.

Add JsonDerivedType; Fix all also resolves newly exposed JSON naming collisions.

Diagnostic declaration · WYRD0301

[JsonDerivedType(typeof(Payment.Charged), "Charged")]
WYRD0302 · ErrorTypes in the JSON context need unique names.

Add a unique TypeInfoPropertyName; Fix all coordinates declarations without duplicates.

Diagnostic declaration · WYRD0302

[JsonSerializable(typeof(Payment.None), TypeInfoPropertyName = "PaymentNone")]
WYRD0200 · WarningIntegration event is missing from the contract snapshot.

Add the reviewed snapshot entry; Fix all coordinates document, project or solution changes.

Diagnostic declaration · WYRD0200

payments.payment-charged v1 | Amount: decimal; Currency: string; OrderId: string
WYRD0201 · ErrorBreaking change in a published integration event.

No dedicated code fix.

Diagnostic declaration · WYRD0201

// Keep V1 unchanged; publish the changed schema as PaymentChargedV2.
WYRD0202 · WarningContract snapshot is outdated.

Update the snapshot entry; Fix all preserves comments and avoids duplicate declarations.

Diagnostic declaration · WYRD0202

// Review the optional field, then apply the snapshot code fix.
WYRD0203 · ErrorA published integration event was removed.

No dedicated code fix.

Diagnostic declaration · WYRD0203

retired payments.payment-charged v1 | Amount: decimal; Currency: string; OrderId: string
WYRD0204 · ErrorIntegration event name and version must be unique.

No dedicated code fix.

Diagnostic declaration · WYRD0204

// Give the new contract a distinct name or increment its version.

Exhaustive union switch suppressor

WYRDSPR0001 → CS8509 suppresses the compiler's missing-case warning only when a switch over a Union handles every declared case; WYRD0020 still reports missing cases.

Suppressor source

payment switch
{
    Payment.None => "none",
    Payment.Charged => "charged",
    Payment.Refunded => "refunded",
};

IDE code fixes and Fix all

ProcessStyleCodeFix converts WYRD0013 processes and supports batch Fix all.

ContractSnapshotCodeFix handles WYRD0200 and WYRD0202, preserving snapshot comments and coordinating all entries.

JsonContextCodeFix handles WYRD0300, WYRD0301 and WYRD0302, including collisions revealed by newly declared cases.

For each provider, choose the lightbulb action, then Fix all in document, project or solution. Contract and JSON fixes use dedicated coordinated providers; syntax conversion uses Roslyn's batch provider.