Reference

Invoked runs: outcomes and refusals

Source Invokes.cs, InvokeOutcomeDelivery.cs, InvokeOutcomeReconciler.cs, InvokedRunOutcomes.cs, InvokesStartupValidator.cs, InvokedTrainLauncher.cs

A state declared with Invokes<TTrain, TInput, TOutput> queues one run of the train each time it is entered, and applies the run's outcome to that entry of the state. Declaring one is covered in Invoke a train from a state; the builder methods are under IInvokeBuilder.

Outcome triggers

Each outcome is a trigger of its own kind, named after the invoking state:

TriggerInput
<State>.donethe train's output, as the outcome's input
<State>.failednone
<State>.cancellednone

A trigger enum member cannot contain a dot, so these never collide with a declared trigger. The IR exports them under outcomes, and the TypeScript twin applies them as events with the same guards and reductions as the server. The input mapping of Invokes runs on the server only: the IR does not carry it, and the twin never starts a train. The server never takes an outcome trigger from a caller: Advance refuses one as OUTCOME_BOUND, and a completion that arrives after the machine left the state is a NO_TRANSITION.

Reserved states

Invoking states and every OnDone, OnFailed and OnCancelled target join the machine's reserved states, beside committed states and effect targets.

RuleDetail
Autosavecannot move a draft into or out of an invoking state, or into an outcome target. The generic save refuses every save of a machine that invokes a train as SAVE_NOT_ALLOWED.
Ordinary transition into an outcome targetrefused by Build. A self-loop on the target, and an edge into a target that itself invokes a train, are allowed.
Outcome into the RunsOnce effect's target, or into the start staterefused by Build.
The RunsOnce countan invoked train does not count against the machine's one effect.
Trains per stateat most one.

Entering and leaving the state

EventWritten
Entering the state, by an advance, a send, or IMachineInstances.Start when the initial state invokesin one transaction, on a data context of its own: the snapshot, the run's work queue entry, and the row's server-only invoke token, which is the entry's external id. A crash or a refusal leaves none of them.
Leaving through any declared transitionthe token is cleared in the same write and the run cancelled: a queued run is marked cancelled, and a dispatched one has its cancel flag set, which it reads at its next junction on whichever host runs it.
A self-loop on the invoking statenothing: the state keeps its run.

The enqueue goes through the mediator: the train is found by its canonical name, authorized, its input capped and its subject key stamped. On Postgres the entry's insert wakes the dispatchers on every host when the transaction commits; on SQLite a dispatcher in the same process is woken, and the workers pick the run up at their polling interval (see Latency).

The entry and the run record which machine, instance and owner kind queued them:

Column (trax.work_queue, trax.metadata)Property (WorkQueue, Metadata)
invoking_machineInvokingMachine
invoking_instance_idInvokingInstanceId
invoking_owner_kindInvokingOwnerKind

All three are null on a run no machine invoked. The launcher writes them through CreateWorkQueue.InvokedBy, an InvokedBy(Machine, InstanceId, OwnerKind), and the dispatcher copies them to the run through CreateMetadata.InvokedBy. A run carrying the link cannot be requeued, and its end is delivered to that instance.

Outcomes

The runOutcome
completedOnDone, with the output the run recorded for its machine
failed by its train, or by the scheduler (reaped as stale, failed at dispatch or on startup recovery)OnFailed
cancelled: a timeout, or, for a system-owned instance, an operator's cancel of the instance, of its run, or of its entry before it was dispatchedOnCancelled
cancelled because its state was leftnone: the token was cleared when the state was left
still queued, requeued after a failed dispatch, or runningnone yet

OnDone edges are tried in declaration order, and the first whose guard holds for the output is taken. The run is queued exactly once, runs at least once, and its outcome is applied exactly once.

Delivery

An outcome is applied to the one row whose invoke token is the run's external id, by a single conditional update (UPDATE ... WHERE invoke_token = @token) that also replaces or clears the token. Every other delivery of the same run, from any host, matches nothing and is a NO_TRANSITION that writes nothing; so is a run whose state was left or entered again under a new token.

PathRuns onCovers
Lifecycle hookthe host that ran the train, when it registers the machinethe run, as soon as its terminal write commits
Outcome reconciler (a hosted service)every host that registers machinesa host that died after the terminal write, a run the scheduler failed or cancelled, a cancel before dispatch, a train run on a host that registers no machines
Reconciler settingValue
Sweep intervalStateMachineOptions.InvokeOutcomeSweepInterval, 5 seconds
Sweep interval while a Postgres host hears run-end notificationsInvokeOutcomeSweepIntervalWhileListening, 1 minute; a full sweep each time it subscribes again
Postgresa trigger notifies every host when an invoked run ends, and each reconciler delivers that run at once; notifications are batched and deduplicated
SQLitethe sweep runs at its interval
What a sweep readsone query per page, only instances of the machines its host registers whose run has ended
An instance whose snapshot the host cannot read (written under a newer machine version)left to a host that can; retried after a wait that doubles from 30 seconds to an hour

A completed run writes the output its machine reads in its own terminal write, in the same statement that records it completed, serialized with the names the machine's guards and reductions use. It is not the run's recorded output, which a host may redact, bound or not keep. The copy is never shown on an operator surface.

When an outcome's target invokes a train, applying the outcome queues that run in the same transaction, under the new token. Only a system-owned machine may declare such an edge.

Latency

A turn that invokes a train waits on three things, and each is woken or polled:

StepOn PostgresOn SQLite or InMemory
The queued run reaches a workerthe work queue's insert wakes the dispatchers on every host when it commitsa dispatcher in the same process is woken; the local workers then pick it up at LocalWorkerOptions.PollingInterval (1 second by default)
The finished run's outcome reaches the instanceat once on the host that ran it; any other host hears the run-end notificationat once on the host that ran it; any other host at its reconciler's sweep, InvokeOutcomeSweepInterval
The changed instance reaches its watchersthe change notice, at onceat once when the write was committed in this host; a write another host made at the next poll, ChangeFeedPollInterval (1 second by default)

So on a single SQLite host the wait that remains is the local workers' polling interval. A host that wants a turn in well under a second sets ConfigureLocalWorkers(w => w.PollingInterval = ...) on the scheduler.

OnFailed reasons

An outcome is applied as the state's OnFailed, with a reason, when its own outcome cannot be applied or the run is gone:

ReasonWhen
invoke-outcome-too-largethe output is past 64 KiB, so it was never stored; or the snapshot its reduction produces is past the 64 KiB snapshot cap
invoke-output-unacceptedno OnDone guard accepts the output
invoke-output-unrecordedthe output could not be serialized for the machine
invoke-outcome-rejectedthe chosen edge's reduction threw, or produced a context its target refuses
invoke-next-run-refusedthe target invokes a train of its own, and that run could not be queued
invoke-run-missingneither the run's work queue entry nor its execution record exists, so it can never end

Each is logged at warning level with the run, the machine, the instance and both states, never with the output or the context. When OnFailed (or OnCancelled) cannot be applied either, the token is cleared and the instance stays in the invoking state with no live run, logged at error level with its reason; it leaves through one of its declared transitions. The same write records the state in invoke_stranded_state (SnapshotDraft.InvokeStrandedState), so TraxInvariants reports only an instance that lost its token by mistake; the mark is cleared when the instance gets a run or moves to another state.

A run's records are kept while its outcome is to come: the work queue entry is written in the same transaction that gives the instance its token, nothing deletes a queued entry, and metadata retention keeps an invoked run and its entry while an instance still holds its token.

Ownership and limits

User-owned (the default)System-owned (SystemOwned())
Instances created bythe user's startIMachineInstances.Start only
Moved bythe user's events and the runs' outcomesthe runs' outcomes and IMachineInstances.Advance
Train authorizedagainst the user entering the state, at entry; refused as INVOKE_FORBIDDENin the trusted execution scope, as a scheduled manifest run
Outcome into another invoking staterefused at startupallowed
Live invoked runs per user10 across every machine, or the machine's InvokedRunLimit(n); past it INVOKE_LIMIT_REACHEDnot capped here; the dispatcher's MaxActiveJobs bounds them

A run is live from the entry that queued it until it ends. Leaving its state cancels a queued run at once; a dispatched run counts until it stops at its next junction. A machine's InvokedRunLimit(n) is compared with the same count across all machines, so the most one user can hold is the largest limit among the host's machines.

The scheduler never retries or dead-letters an invoked run (it has no manifest). requeueExecution and the dashboard's Re-queue refuse one with the same reason.

Host refusals

The host refuses to start when a machine invokes a train it cannot queue in the advance's transaction, cancel from another host, or authorize as declared. Each refusal names the machine, the state and the train, junction or member at fault.

RefusedWhy
a train that is not registered with AddMediator(...)the run cannot be queued or dispatched
a plain Junction anywhere in the train's chain, inside a Parallel branch or a routing step's tracks tooonly an EffectJunction reads the run's cancel flag, so leaving the state could not stop it on another host
an IChain<I> whose registered class is not an EffectJunction, or that the container builds with a factorythe same; the junction it runs must be known
a train that is not a ServiceTrainonly a ServiceTrain's run is sealed to its junctions
anything the chain recorder refuses, or a chain that cannot be read outside a requestthe chain cannot be checked
no AddJunctionProgress()it registers the junction effect that reads the cancel flag
no AddMediator(...)nothing can queue the runs
an ITrainExecutionService registered in place of the mediator'sthe runs are queued through the mediator's own, inside the transaction that enters the state
an OnQueue override or DeferQueuePromotion on the trainboth commit on their own, outside the transaction that enters the state
the InMemory providerit has no transactions
on a user-owned machine, a train whose [TraxAuthorize] names roles or a policy that an event entering the state (or the start into an invoking initial state) does not also requireentering the state runs the train as whoever sent the event
on a user-owned machine, a [TraxBroadcast] trainits subscribers see every run's output
on a system-owned machine, a train that declares [TraxAuthorize]the trusted scope does not check user requirements
on a user-owned machine, an OnDone, OnFailed or OnCancelled that enters a state invoking a trainthe next run would be queued with no user present to authorize it
on a user-owned machine, a timer that enters a state invoking a trainthe same
an output type that reaches a [TraxSensitive] memberthe output is reduced into a context stored as plain JSON and returned to the client

The [TraxBroadcast] and [TraxSensitive] refusals are made again at runtime: entering the state throws and queues nothing, and a sensitive output that reaches a run anyway is neither recorded for the machine nor applied to the context; the state goes to its OnFailed instead.

EffectJunction's railway step is sealed, so a subclass cannot skip the cancel check. A slow decider in Decide or Gate runs outside any junction, so no cancel check happens while it runs, and a train started from inside a junction is not cancelled with its parent.

Operator actions

SurfaceBehaviour
The dashboard's State Machines pages, the GraphQL machineInstances and machineInstance queriesevery instance, system-owned and users' drafts alike: its state, timestamps, owner kind, and the runs it invoked, newest first, with the one its state waits on marked live. Never its context, and never a run's input or output.
Runs listed for a system instanceevery run it invoked (a system instance is unique by machine and id)
Runs listed for a user's draftonly its live run, the one its own token names
The dashboard's Cancel on an instance, cancelMachineInstanceone service method, the same outcome and message. Cancels a system-owned instance's live run, and the instance then moves through OnCancelled. Refused for a user's draft, an instance whose state waits on no run, and one whose run has already ended.
cancelExecution, cancelWorkQueueEntry, the dashboard's Cancel on a run's or an entry's pagerefused, with one message on both surfaces, for a run or a queued entry a step of a user's draft started; their bulk forms skip such rows and say how many. A system-owned instance's run can be cancelled, and the instance moves through OnCancelled.

A run records the machine, the instance id and the owner kind that queued it, not the user; a junction of the run learns the user through InvokedRun, which reads the instance's owner. No Trax-provided operation creates or advances a system instance; a host can advance one from its own code through IMachineInstances.Advance under its own authorization, as the Recovery sample's partitionAction does. The dashboard pages are described in Dashboard.

See also