clockworklabs/SpacetimeDB · error · InvalidOperationException
Reducer arguments type {typeof(T).FullName} is not assignabl
Error message
Reducer arguments type {typeof(T).FullName} is not assignable to {typeof(Reducer).FullName}. What it means
InternalCallReducer<T> accepts any T implementing IReducerArgs (which only requires ReducerName + BSATN read/write), but the pending-call machinery stores the args as the Reducer base class so the onReducer callback can re-deliver them. If the runtime type is not a Reducer instance, the method throws InvalidOperationException after the request id was already allocated. Generated reducer wrappers always satisfy this; hand-written args types may not.
Source
Thrown at sdks/csharp/src/SpacetimeDBClient.cs:875
// Note: this method is called from unit tests.
internal void OnMessageReceived(byte[] bytes, DateTime timestamp)
{
_parseQueue.Add(new UnparsedMessage { bytes = bytes, timestamp = timestamp, parseQueueTrackerId = stats.ParseMessageQueueTracker.StartTrackingRequest() });
}
void IDbConnection.InternalCallReducer<T>(T args)
{
if (!webSocket.IsConnected)
{
Log.Error("Cannot call reducer, not connected to server!");
return;
}
var requestId = stats.ReducerRequestTracker.StartTrackingRequest(args.ReducerName);
if (args is not Reducer typedReducer)
{
throw new InvalidOperationException(
$"Reducer arguments type {typeof(T).FullName} is not assignable to {typeof(Reducer).FullName}."
);
}
var encodedArgs = IStructuralReadWrite.ToBytes(args).ToList();
var pendingReducer = new PendingReducerCall
{
Reducer = typedReducer,
};
pendingReducerCalls[requestId] = pendingReducer;
webSocket.Send(new ClientMessage.CallReducer(new CallReducer(
requestId,
0, // v2 parity with Rust SDK: always CallReducerFlags::Default.
args.ReducerName,
encodedArgs
)));
}
View on GitHub (pinned to 524b4487d9)
Solutions
- Derive your args type from SpacetimeDB.Reducer (extend the generated partial reducer) instead of only implementing IReducerArgs
- Call reducers through the generated wrappers (conn.Reducers.MyReducer(...)) rather than driving InternalCallReducer yourself
- Regenerate module bindings so args types come from the same version as the client SDK
Example fix
// before
public readonly struct MyArgs : IReducerArgs { public string ReducerName => "my_reducer"; ... }
conn.InternalCallReducer(new MyArgs()); // throws: not a Reducer
// after
public partial struct MyReducer : SpacetimeDB.Reducer { ... }
conn.Reducers.MyReducer(new MyArgs { ... }); // generated wrapper passes a Reducer-derived type Defensive patterns
Strategy: type-guard
Type guard
static bool IsReducerCall(object args) => args is SpacetimeDB.Reducer;
Try / catch
try { conn.Reducers.MyReducer(args); }
catch (InvalidOperationException e) when (e.Message.Contains("not assignable to"))
{ /* args type must derive from SpacetimeDB.Reducer — regenerate or fix the partial */ } Prevention
- Always call reducers through the generated wrappers
- Regenerate bindings together with SDK upgrades
- Never hand-implement IReducerArgs without deriving from Reducer
When it happens
Trigger: Calling a reducer with a custom args class that implements IReducerArgs (or IStructuralReadWrite + ReducerName) but does not derive from SpacetimeDB.Reducer; invoking IDbConnection.InternalCallReducer directly with such a type; mixing args types generated against a different SDK/module version.
Common situations: Hand-rolling reducer argument types instead of using the generated partial classes; upgrading the module without regenerating bindings so base types diverge.
Related errors
- Argument must be a Uuid
- Argument must be a ConnectionId
- Argument must be a Identity
- Invalid procedure signature.
- Reducer result for unknown request_id {reducerResult.Request
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/1d68e86ce347ff08.
Report an issue: GitHub.