Reference

dispatch mutations

Package Trax.Api.GraphQLSurface GraphQL schemaSource DispatchMutations.cs, TrainTypeModule.cs

The dispatch field on the root mutation type holds one field per train marked with [TraxMutation], the generated fields of DispatchMutations.

Only trains with the attribute appear under dispatch; trains marked [TraxQuery] appear under discover instead.

Each such train gets a single mutation field named after the train (no prefix). Trains with Namespace set are grouped under a sub-namespace (e.g. dispatch { alerts { createAlert } }). The mutation's parameters and behavior depend on the operations passed to the attribute constructor:

  • Run + Queue (default): when no operations are specified (or both GraphQLOperation.Run and GraphQLOperation.Queue are passed), the mutation accepts an optional mode: ExecutionMode parameter (RUN or QUEUE, default RUN) and an optional priority: Int.
  • Run only: the mutation always runs synchronously. No mode or priority parameters.
  • Queue only: the mutation always queues. Has priority but no mode parameter.

Field names

The field name is derived from the train's service interface name (or overridden via [TraxMutation(Name = "...")]):

  1. Strip the I prefix
  2. Strip the Train suffix
  3. Use the result as the field name (camelCase)

For example, IBanPlayerTrain produces banPlayer.

Example

Given a train annotated with [TraxMutation]:

using Trax.Effect.Models.Manifest;
 
public record BanPlayerInput : IManifestProperties
{
    public required string PlayerId { get; init; }
    public required string Reason { get; init; }
}

The schema exposes:

input BanPlayerInput {
  playerId: String!
  reason: String!
}
 
# Run synchronously (default mode)
mutation {
  dispatch {
    banPlayer(input: { playerId: "player-42", reason: "cheating" }) {
      externalId
      metadataId
      output { ... }
    }
  }
}
 
# Queue for async execution
mutation {
  dispatch {
    banPlayer(
      input: { playerId: "player-42", reason: "cheating" }
      mode: QUEUE
      priority: 10
    ) {
      externalId
      workQueueId
    }
  }
}

Response type

Every dispatch mutation returns a single per-train response type with nullable fields. Which fields are populated depends on the execution mode:

type BanPlayerResponse {
  externalId: String!       # always present
  metadataId: Long          # present for RUN, null for QUEUE
  output: BanPlayerOutput   # present for RUN (typed trains only), null for QUEUE
  workQueueId: Long         # present for QUEUE, null for RUN
}
FieldTypeWhen Populated
externalIdString!Always present. RUN: the execution's external id. QUEUE: a 32-character hex id chosen at enqueue, stamped on the work queue entry, and carried by the run the scheduler later dispatches from it (see below)
metadataIdLongRUN mode. Metadata ID of the completed execution
output{OutputType}RUN mode, only for trains with non-Unit output
workQueueIdLongQUEUE mode. Database ID of the created WorkQueue entry

A QUEUE mutation needs something to dispatch the entry. On a host whose store is in memory (UseInMemory() and no database provider) nothing can, so the request is refused with the error code TRAX_QUEUE_UNAVAILABLE and nothing is enqueued; use mode: RUN there. The train's [TraxAuthorize] requirements are checked first, so a caller the train refuses gets TRAX_AUTHORIZATION, as on any other host. A host on a database provider queues whether or not it runs the scheduler, since a scheduler on another host can dispatch the entry.

Queued runs

A QUEUE mutation returns before any run exists, so it has no metadataId. Its externalId is the correlation key: the JobDispatcher copies it onto the execution (trax.metadata.external_id) it creates for the entry, so the run's lifecycle events, its operations.executions row and the work queue entry (trax.work_queue.external_id) all carry the same value. If a delivery to the runner fails and the entry is requeued (MaxDispatchAttempts), each attempt gets its own execution row with that same external id, and the failed attempts it requeues are recorded Failed by the dispatcher without a lifecycle event. The attempt that fails for good (its attempts exhausted, or its stored input unreadable) is the exception: no runner started it, so the dispatcher publishes its failure (onTrainFailed and a FAILED onTrainStateChanged, with failureException DispatchFailed and a fixed reason; the cause stays on the row). Otherwise only an attempt a runner actually started emits events. Watch a queued run follows one from a client.

A queued run that fails is not retried: retries belong to manifests, and a work queue entry with no manifest runs once. An operator requeues a failed queued run with requeueExecution or the dashboard's Re-queue button.

The wrapper is named {TrainName}Response by default. If the train's output CLR class is also named {TrainName}Response (for example IAddressValidationTrain returning AddressValidationResponse), the wrapper falls back to {TrainName}MutationResponse so the schema can build without a name collision. The output type's name is the one HotChocolate gives it, its [GraphQLName] when it has one. Trains whose output type follows a different naming convention are unaffected. If the fallback name is itself taken, by the response type of a train named {TrainName}Mutation, the host refuses to start naming both trains; set Name on one of them.

Run and queue mode

When no operations are specified (or both GraphQLOperation.Run and GraphQLOperation.Queue are passed), the mutation includes a mode parameter:

ParameterTypeRequiredDefaultDescription
input{TrainName}Input!YesN/AStrongly-typed input matching the train's input record
modeExecutionModeNoRUNWhether to run synchronously (RUN) or queue for async execution (QUEUE)
priorityIntNo0Dispatch priority (0-31, higher runs first). Silently ignored for RUN mode.

The ExecutionMode enum is automatically registered in the GraphQL schema when any train uses both Run and Queue operations:

enum ExecutionMode {
  RUN
  QUEUE
}

Example: run and queue with a typed output

A train ServiceTrain<LookupPlayerInput, LookupPlayerOutput> annotated with [TraxMutation] (default, both modes) produces:

type LookupPlayerResponse {
  externalId: String!
  metadataId: Long
  output: LookupPlayerOutput
  workQueueId: Long
}
 
type LookupPlayerOutput {
  playerId: String!
  rank: Int!
  wins: Int!
  losses: Int!
  rating: Int!
}
 
# Run synchronously (default)
mutation {
  dispatch {
    lookupPlayer(input: { playerId: "player-42" }) {
      externalId
      metadataId
      output {
        playerId
        rank
        wins
        losses
        rating
      }
    }
  }
}
 
# Queue for async execution
mutation {
  dispatch {
    lookupPlayer(
      input: { playerId: "player-42" }
      mode: QUEUE
      priority: 5
    ) {
      externalId
      workQueueId
    }
  }
}

The output type is automatically registered as a GraphQL ObjectType and deduplicated. If multiple trains share the same output type, only one GraphQL type is generated.

Run-only mode

When GraphQLOperation.Run is the only operation passed (e.g. [TraxMutation(GraphQLOperation.Run)]), the mutation always runs synchronously. No mode or priority parameters are generated.

ParameterTypeRequiredDescription
input{TrainName}Input!YesStrongly-typed input matching the train's input record

The response type still uses the unified format, but workQueueId will always be null.

Queue-only mode

When GraphQLOperation.Queue is the only operation passed (e.g. [TraxMutation(GraphQLOperation.Queue)]), the mutation always queues. No mode parameter is generated, but priority is available.

ParameterTypeRequiredDefaultDescription
input{TrainName}Input!YesN/AStrongly-typed input matching the train's input record
priorityIntNo0Dispatch priority (0-31, higher runs first)

The response type still uses the unified format, but metadataId and output will always be null.

See also