Skip to content

Inbound Commands

Inbound commands are messages sent from the CritterWatch server to monitored services. They implement either IServiceMessage (fire-and-forget control commands) or IServiceQuery (request/response queries that return data) and are routed directly to the target service's CritterWatch listener.

All commands are automatically handled when AddCritterWatchMonitoring() is called — no additional handler registration is needed.

The signatures shown below are the live record declarations from Wolverine.CritterWatch/Messages/Inbound/*.cs. The companion await bus.SendAsync(...) line for each is the minimal call you'd issue from a custom integration; in CritterWatch's own UI flows these are emitted from the SignalR hub on operator action.

Operations gating: every mutating command respects the global "Operations Enabled" flag. When disabled, the BFF refuses to send and the matching UI button renders disabled. Queries (IServiceQuery) are always allowed.


DLQ Commands

ReplayMessages

Replay specific dead-lettered messages back through the processing pipeline. The IDs are the envelope ids returned from a DLQ summary or query.

cs
public record ReplayMessages(string ServiceName, Uri StoreUri, Guid[] IdList) : IServiceMessage;
cs
await bus.SendAsync(new ReplayMessages(
    "trip-service",
    new Uri("postgresql://localhost/trip-db"),
    [envelopeId]));

DiscardMessages

Permanently remove dead-lettered messages from the queue.

cs
public record DiscardMessages(string ServiceName, Uri StoreUri, Guid[] IdList) : IServiceMessage;
cs
await bus.SendAsync(new DiscardMessages(
    "trip-service",
    new Uri("postgresql://localhost/trip-db"),
    [envelopeId]));

EditScheduledMessage

Edit a scheduled message's body and/or scheduled time before it fires. Either edit may be null. The MessageId is the envelope id of the scheduled message.

cs
public record EditScheduledMessage(
    Guid MessageId,
    string? EditedBodyJson,
    DateTimeOffset? NewScheduledTime) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new EditScheduledMessage(
    messageId,
    EditedBodyJson: """{"amount":42}""",
    NewScheduledTime: DateTimeOffset.UtcNow.AddHours(1))
    { ServiceName = "trip-service" });

Listener Commands

PauseListener

Pause consumption on a single receiving endpoint across the entire application, durably. Every node stops pulling messages off that queue, and the pause is persisted as an agent restriction that survives restarts and new nodes joining the cluster — it stays paused until you RestartListener, with no auto-resume. Messages stay safely on the broker. In-flight work is not waited on; for a graceful shutdown that lets queued and in-flight messages finish first, use DrainListener instead.

(Under single-node / DurabilityMode.Solo hosts, where there is no cluster to coordinate, this falls back to latching the local listener — the effect is the same: consumption stops until resumed.)

cs
public record PauseListener(string ServiceName, string EndpointUri) : IServiceMessage;
cs
await bus.SendAsync(new PauseListener("trip-service", "rabbitmq://exchange/trips/"));

RestartListener

Resume a durably-paused (or drained) listener: clears the pause restriction across the application and restarts consumption on every node. The accumulated backlog on the broker drains down once consumption resumes.

cs
public record RestartListener(string ServiceName, string EndpointUri) : IServiceMessage;
cs
await bus.SendAsync(new RestartListener("trip-service", "rabbitmq://exchange/trips/"));

DrainListener

Graceful shutdown of a single listener — stops accepting new messages, lets queued and in-flight messages finish, then halts. The right call before a deploy or a planned-maintenance window. Distinct from PauseListener which abandons in-flight work.

cs
public record DrainListener(string ServiceName, string EndpointUri) : IServiceMessage;
cs
await bus.SendAsync(new DrainListener("trip-service", "rabbitmq://exchange/trips/"));

PauseAllListeners

Pause every receiving endpoint in the service — the durable, application-wide pause of PauseListener applied to all endpoints in one round trip. Useful before an emergency maintenance window.

cs
public record PauseAllListeners(string ServiceName) : IServiceMessage;
cs
await bus.SendAsync(new PauseAllListeners("trip-service"));

RestartAllListeners

Resume every listener in the service — clears all pause restrictions and restarts consumption across the application.

cs
public record RestartAllListeners(string ServiceName) : IServiceMessage;
cs
await bus.SendAsync(new RestartAllListeners("trip-service"));

Endpoint Configuration Commands

UpdateEndpointBufferingLimits

Adjust the in-memory buffering thresholds on a single endpoint at runtime. Maximum is MaximumMessagesToReceive; Restart is the recovery threshold below which buffering resumes.

cs
public record UpdateEndpointBufferingLimits(string EndpointUri, int Maximum, int Restart) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new UpdateEndpointBufferingLimits(
    "rabbitmq://exchange/trips/",
    Maximum: 1000,
    Restart: 200)
    { ServiceName = "trip-service" });

UpdateEndpointCircuitBreaker

Adjust the circuit-breaker configuration on a single endpoint at runtime.

cs
public record UpdateEndpointCircuitBreaker(string EndpointUri, double FailurePercentageThreshold, TimeSpan PauseTime) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new UpdateEndpointCircuitBreaker(
    "rabbitmq://exchange/trips/",
    FailurePercentageThreshold: 0.25,
    PauseTime: TimeSpan.FromMinutes(2))
    { ServiceName = "trip-service" });

Projection Commands

All four projection commands accept an optional TenantId. Leave it null (the default) to act on the store-global shard — the existing single-tenant behavior, byte-for-byte. Set it to a tenant id to scope the operation to that one tenant's shard for the projection. The CritterWatch UI fills it in automatically based on the Tenants-tab row you act from; integrations sending these commands directly (custom UIs, ops scripts, the HTTP API) need to set it themselves.

Per-tenant operations respect tenant-scoped RBAC — a user authorized for only some tenants can't rebuild another tenant's projection. See Multi-Tenancy → Per-tenant scoping for the bigger picture.

PauseProjection

Pause a single projection shard. The shard stops advancing until restarted.

cs
public record PauseProjection(string ServiceName, Uri AgentUri) : IServiceMessage
{
    public string? TenantId { get; init; }

    /// <summary>Client-supplied correlation id, echoed back on the ProjectionCommandAck so the
    /// UI can map the acknowledgement to the originating action. Null for non-UI callers.</summary>
    public string? RequestId { get; init; }
}
cs
await bus.SendAsync(new PauseProjection(
    "trip-service",
    new Uri("marten://projection/TripSummary:All")));

// Per-tenant variant
await bus.SendAsync(new PauseProjection(
    "trip-service",
    new Uri("marten://projection/TripSummary:All"))
    { TenantId = "acme-corp" });

RestartProjection

Resume a paused projection shard.

cs
public record RestartProjection(string ServiceName, Uri AgentUri) : IServiceMessage
{
    public string? TenantId { get; init; }

    /// <summary>Client-supplied correlation id, echoed back on the ProjectionCommandAck so the
    /// UI can map the acknowledgement to the originating action. Null for non-UI callers.</summary>
    public string? RequestId { get; init; }
}
cs
await bus.SendAsync(new RestartProjection(
    "trip-service",
    new Uri("marten://projection/TripSummary:All")));

RebuildProjection

Reset a projection's state and rebuild from the beginning of its source stream. Long-running on big stores; while it runs, the shard's threshold-based alerts should be suppressed via Alert Configuration → Projections → Per-Shard Overrides.

TenantId rebuilds only that tenant's read model — useful when a single tenant's projection has drifted (a code bug, a manual data fix) and you don't want to spend the rebuild cost on every tenant. There is no "rebuild for all tenants" button today; each tenant is one click.

cs
public record RebuildProjection(string ServiceName, Uri AgentUri) : IServiceMessage
{
    /// <summary>
    /// #263 Phase 3a / #209 — when non-null, the rebuild is scoped to
    /// the named tenant via <c>ShardName.ForTenant(name, tenantId)</c>
    /// and dispatched to the per-tenant daemon API
    /// (<c>RebuildProjectionAsync(name, tenantId, …)</c>). Null
    /// preserves the existing store-global rebuild verbatim.
    /// </summary>
    public string? TenantId { get; init; }

    /// <summary>Client-supplied correlation id, echoed back on the ProjectionCommandAck so the
    /// UI can map the acknowledgement to the originating action. Null for non-UI callers.</summary>
    public string? RequestId { get; init; }
}
cs
// Single-tenant service — or a global rebuild on a multi-tenant store.
await bus.SendAsync(new RebuildProjection(
    "trip-service",
    new Uri("marten://projection/TripSummary:All")));

// Per-tenant rebuild
await bus.SendAsync(new RebuildProjection(
    "trip-service",
    new Uri("marten://projection/TripSummary:All"))
    { TenantId = "acme-corp" });

RewindSubscription

Move a subscription's position back to a chosen point. The mode controls which target field applies.

cs
public record RewindSubscription(
    string AgentUri,
    RewindMode Mode,
    long? TargetSequence,
    DateTimeOffset? TargetTimestamp) : IServiceMessage
{
    public string ServiceName { get; init; } = "";

    /// <summary>
    /// #263 Phase 3a / #209 — when non-null, the rewind is scoped to
    /// the named tenant. Null preserves the existing store-global
    /// rewind verbatim.
    /// </summary>
    public string? TenantId { get; init; }

    /// <summary>Client-supplied correlation id, echoed back on the ProjectionCommandAck so the
    /// UI can map the acknowledgement to the originating action. Null for non-UI callers.</summary>
    public string? RequestId { get; init; }
}

public enum RewindMode
{
    ToBeginning,
    ToSequence,
    ToTimestamp
}
cs
await bus.SendAsync(new RewindSubscription(
    "marten://subscription/AnalyticsExporter",
    RewindMode.ToTimestamp,
    TargetSequence: null,
    TargetTimestamp: DateTimeOffset.UtcNow.AddHours(-1))
    { ServiceName = "trip-service" });

EjectProjection

Permanently delete the leftover progression row for an orphaned shard — one whose projection has been renamed, versioned, or removed, so it shows up as an "Orphaned" card and can never be paused, restarted, or rebuilt (no registered projection owns it). Ejecting drops the row outright through the store-agnostic IEventDatabase.DeleteProjectionProgressByShardNameAsync (Marten 9.11 / Polecat 4.6); the orphan card then reconciles out of the UI on the next progression poll. The match is on the exact shard identity, so siblings are never collaterally dropped, and a non-existent identity is a clean no-op. This is irreversible — a still-registered projection would simply re-create its row from zero, so only eject genuine orphans.

StoreUri targets the owning store on a multi-store (ancillary) service; omit it on single-store services.

cs
public record EjectProjection(string ServiceName, string ShardName) : IServiceMessage
{
    /// <summary>
    /// The owning store's clean identity (the <c>marten://…</c> / <c>polecat://…</c> <c>StoreUri</c> the
    /// progression poller stamps on each shard state). Selects the registered store whose
    /// <c>IEventStore.Subject</c> matches — needed on multi-store (ancillary) services to avoid ejecting a
    /// same-named row from the wrong store. Null on single-store services ⇒ every registered store is
    /// targeted (the delete is an exact-identity, zero-row no-op against stores that don't hold the row).
    /// </summary>
    public string? StoreUri { get; init; }
}
cs
// Delete the leftover progression row for an orphaned shard (a renamed / versioned /
// removed projection). Pass the raw shard identity exactly as surfaced on the Orphaned card.
await bus.SendAsync(new EjectProjection(
    "trip-service",
    "TripSummary:V1:All"));

// On a multi-store (ancillary) service, name the owning store so a same-named row in
// another store is left untouched.
await bus.SendAsync(new EjectProjection(
    "trip-service",
    "TripSummary:V1:All")
    { StoreUri = "marten://main" });

Agent Commands

PinAgentToNode

Force an agent to run on a specific node and prevent the leader from rebalancing it elsewhere. Useful when a particular node has hardware or licensing affinity (a GPU, a license-restricted IP, etc.).

cs
public record PinAgentToNode(string ServiceName, Uri AgentUri, int NodeNumber) : IServiceMessage;
cs
await bus.SendAsync(new PinAgentToNode(
    "trip-service",
    new Uri("marten://projection/TripSummary:All"),
    NodeNumber: 2));

UnpinAgent

Remove a pin so the leader can rebalance the agent again.

cs
public record UnpinAgent(string ServiceName, Uri AgentUri) : IServiceMessage;
cs
await bus.SendAsync(new UnpinAgent(
    "trip-service",
    new Uri("marten://projection/TripSummary:All")));

PushAgentThresholds

Push updated behind-high-water-mark warning + critical thresholds to a specific shard for client-side validation. Mirrors the values stored in Alert Configuration → Projections → Per-Shard Overrides so the service can refuse work that would never satisfy the operator's stated SLO.

cs
public record PushAgentThresholds(string ShardName, long? WarningBehind, long? CriticalBehind) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new PushAgentThresholds(
    "TripSummary:All",
    WarningBehind: 1000,
    CriticalBehind: 10000)
    { ServiceName = "trip-service" });

RequestAgentHealthReport (query)

Ask the service to immediately publish an agent-health snapshot, bypassing the periodic health-report timer. Implements IServiceQuery.

cs
public record RequestAgentHealthReport : IServiceQuery
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.InvokeAsync(new RequestAgentHealthReport { ServiceName = "trip-service" });

RequestHandlerSourceCode (query)

Ask the service to send the generated handler source code for a given message type. The optional EndpointUri selects the endpoint-specific specialisation when sticky routing applies.

cs
public record RequestHandlerSourceCode(string HandlerMessageType, string? EndpointUri) : IServiceQuery
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.InvokeAsync(new RequestHandlerSourceCode(
    "Trips.CompleteTrip",
    EndpointUri: null)
    { ServiceName = "trip-service" });

RequestEndpointProperties (query)

Lazy-fetch the Properties + Children configuration tree for one Wolverine endpoint. The Pipeline tab issues this when the operator opens an endpoint row; the response is cached on the console keyed by service version. See Observer → Lazy-fetched detail panes for the full caching shape.

cs
public record RequestEndpointProperties(string EndpointUri) : IServiceQuery
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.InvokeAsync(new RequestEndpointProperties(
    "rabbitmq://exchange/trips/")
    { ServiceName = "trip-service" });

RequestMessageHandlerProperties (query)

Lazy-fetch the per-handler Properties rows for one message type. The handler-chain detail page issues this when opened. Returns one row per handler chain registered against the message type.

cs
public record RequestMessageHandlerProperties(string MessageType) : IServiceQuery
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.InvokeAsync(new RequestMessageHandlerProperties(
    "Trips.CompleteTrip")
    { ServiceName = "trip-service" });

Node Commands

EjectNode

Remove one or more stale nodes from the cluster. The leader redistributes their assigned agents.

cs
public record EjectNode(string ServiceName, int[] NodeNumbers) : IServiceMessage;
cs
await bus.SendAsync(new EjectNode("trip-service", [3]));

TriggerElection

Force a new leader election. The current leader's lease is revoked and the cluster votes again. Disruptive — only fire when the cluster is genuinely stuck.

cs
public record TriggerElection(string ServiceName) : IServiceMessage;
cs
await bus.SendAsync(new TriggerElection("trip-service"));

ClearNodeHistory

Trim the historical-node log, retaining the most recent RetainRecords entries. The optional Timestamp is the cutoff before which records are eligible for removal.

cs
public record ClearNodeHistory(string ServiceName, int RetainRecords, DateTimeOffset? Timestamp) : IServiceMessage;
cs
await bus.SendAsync(new ClearNodeHistory(
    "trip-service",
    RetainRecords: 10,
    Timestamp: null));

Tenant Commands

All tenant commands carry an implicit ServiceName via IServiceMessage's ServiceName property; the explicit parameters are just the tenant fields.

AddTenant

Add a new tenant database to a multi-tenant service. The connection string is sent over SignalR; rotate credentials after the cutover if you want a clean audit trail.

cs
public record AddTenant(string TenantId, string ConnectionString) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new AddTenant(
    "acme-corp",
    "Host=db;Database=acme;Username=...;Password=...")
    { ServiceName = "trip-service" });

DisableTenant

Soft-disable a tenant. Messages addressed to the tenant queue without losing data; can be re-enabled with EnableTenant.

cs
public record DisableTenant(string TenantId) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new DisableTenant("acme-corp") { ServiceName = "trip-service" });

EnableTenant

Re-enable a previously disabled tenant.

cs
public record EnableTenant(string TenantId) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new EnableTenant("acme-corp") { ServiceName = "trip-service" });

RemoveTenant

Remove a tenant's master-table record. The per-tenant database itself is not dropped — it stays around for any forensic work or restore scenario.

cs
public record RemoveTenant(string TenantId) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new RemoveTenant("acme-corp") { ServiceName = "trip-service" });

HardDeleteTenant

Drop the tenant database and remove the master-table record. Permanent. The CritterWatch UI gates this behind a typed-tenant-id confirmation modal — see Services → Tenants tab. License-gated to Professional+ on multi-tenant deployments.

cs
public record HardDeleteTenant(string TenantId) : IServiceMessage
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.SendAsync(new HardDeleteTenant("acme-corp") { ServiceName = "trip-service" });

RequestTenantList (query)

Ask the service to publish its current tenant list. Used by the Tenants tab's Refresh button. Implements IServiceQuery.

cs
public record RequestTenantList : IServiceQuery
{
    public string ServiceName { get; init; } = "";
}
cs
await bus.InvokeAsync(new RequestTenantList { ServiceName = "trip-service" });

See Also

  • Outbound Events — events services publish to CritterWatch
  • Registration — installing the integration in your service
  • Multi-Tenancy — full tenant management flow
  • Audit Log — every command emits an audit entry; use the log to reconstruct what was sent and when

Free for read-only monitoring. A commercial license is required for administrative actions and the MCP server.