The canonical per-domain catalog (framework §2.22.1), anchored at the
Elsa.Workflows.Runtime composition root. Contracts remain in Elsa.Workflows.Runtime.Core;
provider/default implementations may live in sibling runtime projects. Runtime execution
contracts are Design-free and operate on runtime-owned executable artifacts and execution state.
ADR 0033 split. Since the contracts/engine split,
Elsa.Workflows.Runtime.Coreholds only the contracts, models, constants, pipeline contract surface (middleware attribute/bases/placeholders,RuntimePipelinePlanBuilder), and validators. Unless an entry states otherwise, every default implementation named in this catalog now lives in thisElsa.Workflows.Runtimeimplementation package (moved types keep theirElsa.Workflows.Runtime.Core.*namespaces). Replacing a default still works the same way: register your implementation against the.Corecontract; the engine package's registrations all useTryAdd*.
Two contract-shaped surfaces live in the engine package rather than .Core, deliberately:
IRuntimeCoalescingSessionAccessor+IRuntimeCoalescingDrainScopeFactory(namespaceElsa.Workflows.Runtime.Core.Contracts) expose the concreteRuntimeCoalescingSessionengine working state. Their only consumers are the opt-in coalescing composition in Runtime.Api and its tests.ActivityRuntimePipelineBuilder+WorkflowRuntimePipelineBuilder(namespaceElsa.Workflows.Runtime.Core.Builders) bake concrete engine middleware and the concreteRuntimeCheckpointCommitterinto their default plans. The declarative slot machinery remains in.Core, so third-party middleware authors do not need the engine package at compile time.
The ADR 0033 RuntimeCoreEngineShapeGuardTests architecture guard prevents concrete engine-role
types (*Service, *Handler, *Dispatcher, *Drainer, *Orchestrator, *Materializer,
*Committer, *Scheduler, *Router, *Pipeline, *Session, *Scanner, *Processor, and
InMemory*) from moving back into the .Core assembly.
- Kind: Replacement (one provider owns retained workflow-execution state and its executable-retention projection).
- Signature: in addition to save/find/list,
ListPinnedExecutableArtifactIdsAsync(...)returns the distinct artifact IDs pinned by every retained execution status, andDeleteAsync(workflowExecutionId, ...)removes an execution under the host's retention policy. - Usage: workflow-execution records are durable executable-retention roots. Completion or fault does not release an artifact; only deletion of the retained execution does. Providers must answer the distinct-root query without materializing every full workflow-execution document and must keep the projection consistent with save/delete.
- Default implementation:
InMemoryWorkflowExecutionStateStore; durable persistence providers such as Groundwork replace it.
- Kind: Replacement (one collector owns physical executable-artifact reclamation for a runtime composition).
- Signature:
SweepAsync(CancellationToken cancellationToken = default). - Usage: an artifact is eligible only when it is outside creation/staging grace and absent from both root sets: live source references and retained workflow executions. Root writers acquire a provider-backed executable lease before committing either root. The collector first acquires a conditional deletion guard, then checks both root sets, and only that matching guard may delete; leases and guards use provider CAS so the check/delete boundary is safe across hosts. Root-query or guard failures retain the artifact for a later sweep.
- Default implementation:
WorkflowExecutableReferenceGarbageCollector; registered by the Runtime composition root. The opt-inWorkflowsRuntimeReferenceGarbageCollectionfeature schedules it and exposes cadence/grace policy.
- Kind: Replacement (one coordinator scopes the provider-backed lease required to establish an executable-retention root).
- Signature:
ExecuteAsync(artifactId, leaseId, write, ...)runs the durable write while acquiring, renewing, and finally releasing its lease. - Usage: canonical publication, test-run, and workflow-execution checkpoint writers execute their durable root write through this coordinator. Persistence providers implement the underlying lease/guard transitions on
IWorkflowExecutableStore; custom root writers must use the same coordinator. Lease loss cancels the write and is surfaced rather than silently reporting an unprotected root. - Default implementation:
WorkflowExecutableRootWriteLeaseManager.
- Kind: Replacement (one policy decides how checkpoints flush in a runtime composition).
- Signature:
DecideAsync(RuntimeCheckpoint checkpoint, CancellationToken cancellationToken = default). - Usage: separates checkpoint semantics from persistence timing. The checkpoint name says what changed; the policy decides immediate, deferred, or skipped flush.
- Default implementation:
ImmediateRuntimeCheckpointPersistencePolicy(intra-domain default). - Alternative implementation:
CoalescingRuntimeCheckpointPersistencePolicy(opt-in, W9/E3-6/RT-10) — burst-coalescing folding of intra-drain checkpoints into one flush at quiescence; enable withservices.AddCoalescingRuntimeCheckpointPersistence()(see the Coalescing checkpoint persistence section below).
- Kind: Replacement (one writer owns persistence of checkpoint envelopes for a runtime composition).
- Signature:
WriteAsync(RuntimeCheckpointCommit commit, RuntimeCheckpointPersistenceDecision decision, CancellationToken cancellationToken = default). - Usage: implemented by runtime persistence providers to commit the checkpoint boundary and its atomic state-change envelope.
- Default implementation:
InMemoryRuntimeCheckpointWriter(single-node in-memory default for the current runtime slice; durable providers replace this).
- Kind: Replacement (one dispatcher owns delivery of committed outbound runtime intents for a composition).
- Signature:
DispatchAsync(RuntimePostCommitIntent intent, CancellationToken cancellationToken = default). - Default implementation:
RuntimeSchedulerPostCommitIntentDispatcher(dispatches scheduler-work intents after checkpoint commit; durable outbox providers replace this for distributed delivery). - Usage: dispatches post-commit intents in the order provided by the committed
RuntimeCheckpointCommitonly afterIRuntimeCheckpointWritercompletes successfully. This is a placeholder contract, not a full outbox processor.
- Kind: Replacement (one provider owns durable post-commit outbox state for a runtime composition).
- Signature:
SavePendingAsync(RuntimePostCommitOutboxItem item, ...),GetDeliverableAsync(RuntimePostCommitOutboxQuery query, ...),RecordDeliveryResultAsync(RuntimePostCommitOutboxDeliveryResult result, ...). - Usage: stores delivery state for post-commit intents so providers can preserve record, commit, deliver, and mark-delivered ordering.
- Default implementation:
InMemoryRuntimePostCommitOutboxStore(single-node in-memory default for the current runtime slice; durable providers replace this).
- Kind: Replacement (one scanner identifies interrupted workflow executions for a runtime composition).
- Signature:
ScanAsync(RuntimeRecoveryScanRequest request, CancellationToken cancellationToken = default). - Usage: provider implementations inspect operational state such as leases and heartbeats and return recovery candidates that requeue from the last checkpoint without invoking domain retry policy.
- Default implementation:
InMemoryRuntimeRecoveryScanner(single-node in-memory default for operational recovery candidate discovery).
- Kind: Replacement (one service owns a single system-wide resumption sweep pass for a runtime composition).
- Signature:
SweepAsync(RuntimeResumptionSweepRequest request, CancellationToken cancellationToken = default). - Usage: one sweep pass re-delivers stranded post-commit outbox items system-wide (
ProcessAsync(workflowExecutionId: null, intentKind: EnqueueSchedulerWork)), unions durable scheduler-queue backlog (IWorkflowSchedulerWorkQueue.ListPendingWorkflowExecutionIdsAsync) withIRuntimeRecoveryScannercandidates, and re-drives each discovered execution by enqueueing aRunSchedulerWorkenvelope through the actor mailbox — preserving single-writer discipline. The request bounds each sweep (MaxExecutionsPerSweep) and skips executions the caller is backing off (ExcludedWorkflowExecutionIds). Re-drive failures surface on the result and do not abort the sweep; callers own logging and backoff. It is not registered by the runtime API feature — only theWorkflowsRuntimeResumptionshell feature registers it and drives it from a recurring pump. - Default implementation:
RuntimeResumptionService(registered by the feature-gatedElsa.Workflows.Runtime.Resumptionpackage).
- Kind: Replacement (one policy decides workflow/activity domain retry behavior for a runtime composition).
- Signature:
Decide(RuntimeDomainRetryRequest request). - Usage: keeps workflow/activity retry decisions separate from operational recovery such as lost leases and interrupted execution agents.
- Default implementation:
NoopRuntimeDomainRetryPolicy(explicit do-not-retry baseline; workflow/activity retry policy providers replace this).
- Kind: Replacement (one policy decides how an exception is turned into structured fault information for a runtime composition).
- Signature:
Capture(Exception exception)→RuntimeFaultInfo. - Usage: unifies runtime fault capture (RT-12) so the drainer's handler-crash path and the post-commit outbox delivery path both record the same structured
RuntimeFaultInfo(exception type + message, stack trace behind an opt-in flag) instead of two divergentexception.ToString()/exception.Messagepolicies.RuntimeFaultInfo.ToSummaryString()yields"{ExceptionType}: {Message}". - Default implementation:
DefaultRuntimeFaultCapturePolicy(type full name + message; stack trace only whenRuntimeFaultCaptureOptions.CaptureStackTraceis enabled).
- Kind: Replacement (one tracer owns engine-phase span emission for a runtime composition).
- Signature:
StartDrainCycle(RuntimeSchedulerDrainRequest),StartDispatch(RuntimeSchedulerWorkItem),StartActivityExecution(RuntimeSchedulerWorkItem),StartCheckpointCommit(RuntimeCheckpointCommit)— each returnsActivity?. - Usage: engine self-instrumentation (MS-9). The four hot-path phases (drain → dispatch → activity.execute / checkpoint.commit) start spans on the
Elsa.Workflows.RuntimeActivitySource; nesting is viaActivity.Current, tags are set throughactivity?.SetTag(...)after values exist. Instrumentation is behaviour-preserving: no new awaits inside the fenced drain/commit sequences, no W12 slot reordering, and the no-op path allocates nothing. Span/tag names are the stable contract inWorkflowEngineTelemetry. This is engine telemetry (emits spans), not theElsa.Diagnostics.OpenTelemetryingestion domain (receives OTLP) — seedocs/reference/engine-telemetry.md. - Default implementation:
NullWorkflowEngineTracer(allocation-free no-op; registered by the runtime composition root). - Alternative implementation:
ActivitySourceWorkflowEngineTracer(opt-in — composed by theWorkflowsRuntimeTracingshell feature inElsa.Workflows.Runtime.Tracing, whichservices.Replace(...)s the no-op; still costs nothing until anActivityListener/OpenTelemetryAddSourceattaches).
- Kind: Replacement (one store owns poison/retry records for crashed scheduler work items in a runtime composition).
- Signature:
RecordAsync(RuntimeSchedulerPoisonRecord record, ...),FindAsync(workflowExecutionId, workItemId, ...),ListAsync(workflowExecutionId, ...). - Usage: when a scheduler work handler crashes, the drainer captures the fault, consults
IRuntimeDomainRetryPolicy, and records aRuntimeSchedulerPoisonRecordhere instead of dropping the dequeued item (RT-1 gap b). Disposition isPoisoned(terminal, no retry) orRetryScheduled(carriesNextRetryAt). The defaultNoopRuntimeDomainRetryPolicyyieldsPoisoned— a safe, non-looping baseline. - Default implementation:
InMemoryWorkflowSchedulerPoisonStore(intra-domain default; a durable poison store is future provider work — see follow-ups below). - Follow-ups (W1 → W2):
RetryNowre-enqueues immediately through theIWorkflowSchedulerWorkQueuepublic contract and also recordsRetryScheduled;RetryAfter(delay)recordsRetryScheduledwithNextRetryAtbut does not re-enqueue — re-driving delayed retries is left to the durable resumption pump (RuntimeResumptionPumpTask; seedocs/runtime-durable-resumption.md), which avoids ignoring the delay / hot-looping. A durable poison store and the delayed re-drive are explicit follow-ups, not W1 scope.
- Kind: Replacement (one policy decides whether in-memory volatile waits are allowed in a runtime composition).
- Signature:
Decide(RuntimeVolatileWaitPolicyRequest request). - Usage: evaluates host support, requested duration, requested host-shutdown behavior, requested cancellation behavior, and durable fallback posture. Volatile waits remain scheduler continuation state and are not durable bookmark resume state.
- Default implementation:
DefaultRuntimeVolatileWaitPolicy(allows only when the host explicitly supports in-memory continuation; host-specific providers can replace this).
- Kind: Replacement (one scheduler adapter turns in-workflow generator emissions into ordered runtime scheduler work).
- Signature:
ScheduleAsync(RuntimeGeneratorEmissionScheduleRequest request, CancellationToken cancellationToken = default). - Usage: enqueues
WorkflowExecutionCommandKind.GeneratedEventwork throughIWorkflowSchedulerWorkQueueusing deterministic IDs derived from workflow execution and generated event identity. Generator registrations and generated-event lanes remain scheduler state; this is not a trigger provider, generator execution loop, or separate generator-state store. - Default implementation:
RuntimeGeneratorEmissionScheduler(single-node scheduler queue adapter for the current runtime slice).
- Kind: Replacement (one store owns administrative control-plane state for a runtime composition).
- Signature:
SaveAsync(WorkflowHoldState state, ...),FindAsync(string controlPlaneStateId, ...),ListForWorkflowExecutionAsync(string workflowExecutionId, ...),ListAllAsync(...). - Usage: stores pause/unpause administrative holds outside workflow continuation state. Durable or distributed control-plane providers can replace the default without changing workflow execution state contracts.
- Default implementation:
InMemoryWorkflowHoldStateStore(single-node in-memory default for the current runtime slice).
- Kind: Replacement (one provider decides whether runtime scheduler work may advance through named pause boundaries).
- Signature:
DecideAsync(RuntimePauseDecisionRequest request, CancellationToken cancellationToken = default). - Usage: evaluates active control-plane holds at safe runtime boundaries and returns
SchedulerPauseDecision. Pause/unpause remain control-plane operations and are not durable suspend/resume or volatile continue semantics. - Default implementation:
RuntimePauseDecisionProvider(matches effective holds by workflow/activity/generator/ingress/worker/host target and picks oldest hold then hold ID deterministically).
- Kind: Replacement (one resolver owns durable bookmark-to-artifact resume resolution for a runtime composition).
- Signature:
Resolve(BookmarkResumeRequest request). - Usage: maps
BookmarkState.ResumeTargetIdthrough the pinnedWorkflowExecutable.ResumeTargetstable and returns the executable node plus runtime resume target. It does not load artifacts, invoke activity handlers, or implement the bookmark store. - Default implementation:
BookmarkResumeResolver(intra-domain default).
- Kind: Replacement (one lookup surface finds bookmark continuation state for a workflow execution and stimulus identity).
- Signature:
FindAsync(BookmarkStimulusLookupRequest request, CancellationToken cancellationToken = default). - Usage: matches non-expired
BookmarkStaterecords by workflow execution ID, stimulus type, and stimulus hash. Ambiguous matches are rejected instead of guessed. Durable providers can replace the default with indexed lookup. - Default implementation:
BookmarkStimulusLookup(list-based default overIBookmarkStateStorefor the current in-memory slice).
- Kind: Replacement (one dispatcher turns matched bookmark stimuli into workflow execution mailbox commands).
- Signature:
DispatchAsync(BookmarkResumeDispatchRequest request, WorkflowExecutionCommandDispatchOptions? dispatchOptions = null, CancellationToken cancellationToken = default). (spec 089 E) - Usage: uses
IBookmarkStimulusLookup, workflow execution state, the pinned executable artifact, andIBookmarkResumeResolverto enqueue aResumeBookmarkcommand throughIWorkflowExecutionActorProvider(threadingdispatchOptions ?? Default). The command payload carriesResumeTargetId, not C# callback method names. It does not consume bookmarks or invoke activity resume handlers. Ambient-services passthrough (spec 089 E-D4): asIWorkflowStartDispatcher,dispatchOptions.AmbientServicesreaches the inline resume drain — so scenario 5.5's resuming request writes the workflow's subsequent live response in its own exchange (each resuming request dispatches with its own request scope). Same never-durable / never-cross-process invariant. - Default implementation:
BookmarkResumeDispatcher.
- Kind: Replacement (narrow cross-execution read surface over bookmark state; segregated from
IBookmarkStateStoreso it can be widened/replaced independently). - Signature:
ListByStimulusAsync(string stimulusType, string stimulusHash, CancellationToken cancellationToken = default);ListByStimulusTypeAsync(string stimulusType, CancellationToken cancellationToken = default)(spec 089 D). - Usage: returns raw
BookmarkStaterecords matching a stimulus across every workflow execution (E3-5 fan-in), unlikeIBookmarkStimulusLookupwhich is scoped to oneworkflowExecutionId.ListByStimulusAsyncmatches (type, hash);ListByStimulusTypeAsync(spec 089 D) is a hash-agnostic type-scoped scan that enumerates every waiting bookmark of a stimulus family (e.g. allHttpEndpointbookmarks) so the route-table resolver can union their templates. Both are raw scans — neither filters expiry or correlation (that stays inIGlobalBookmarkStimulusLookup). Implemented by the bookmark state store itself (in-memory and Groundwork).ListByStimulusAsyncuses the additivebookmarkStateby-stimulus(hash) index;ListByStimulusTypeAsync— like the siblingGroundworkWorkflowTriggerBindingStore.ListByStimulusTypeAsync(spec 089 B) — does a clause-free full scan narrowed by type in code (the hash index cannot serve a type-only query), so NO new index is added andSchemaVersionis unchanged; it feeds the route-table refresh, not a hot per-request path. Note the Condition 7 gap (seedocs/serialization.md): bookmarks written before theby-stimulusindex existed are not backfilled until re-saved. - Default implementation:
InMemoryBookmarkStateStore/GroundworkBookmarkStateStore(each also implements this interface).
- Kind: Replacement (one cross-execution lookup surface finds every waiting bookmark for a stimulus).
- Signature:
FindWaitingAsync(GlobalBookmarkStimulusLookupRequest request, CancellationToken cancellationToken = default);FindWaitingByTypeAsync(GlobalBookmarkStimulusTypeLookupRequest request, CancellationToken cancellationToken = default)(spec 089 D). - Usage: builds the fan-in resume set for the stimulus router by querying
IBookmarkStimulusIndex, filtering expired bookmarks against the evaluated time and (when supplied) a passive correlation scope carried in bookmark metadata. Correlation is a threaded metadata value only — not a correlation subsystem.FindWaitingByTypeAsync(spec 089 D) is the type-scoped counterpart used by the mid-flow HttpEndpoint route-table resolver and middleware: it returns the non-expiredMatchessnapshots (incl.Metadata) for a stimulus type regardless of hash, so a consumer can read the durable route template + endpoint options a mid-flow suspension stored. Expiry filtering lives here, not in the raw index; no correlation scoping (mid-flow bookmark resumes are instance-scoped). - Default implementation:
GlobalBookmarkStimulusLookup.
- Kind: Replacement (one provider owns the durable trigger-binding index for a runtime composition).
- Signature:
SaveAsync(...),ListByStimulusAsync(stimulusType, stimulusHash, ...),ListByStimulusTypeAsync(stimulusType, ...),ListByArtifactAsync(artifactId, ...),DeleteByArtifactAsync(artifactId, ...). - Usage: stores
WorkflowTriggerBindingdocuments mapping a stimulus identity to a start-trigger inside a published artifact.ListByStimulusis the cross-artifact fan-out the router uses to start every workflow waiting on a stimulus;ListByStimulusTypeis a type-scoped full scan (no hash) used to rebuild a per-shell projection over one stimulus family (e.g. the HTTP route table);by-artifactscoping supports republish replacement. - Default implementation:
InMemoryWorkflowTriggerBindingStore(single-node in-memory default;GroundworkWorkflowTriggerBindingStorereplaces it for durable storage over theworkflowTriggerBindingdocument kind).
- Kind: Replacement (one extractor derives trigger bindings from a published executable).
- Signature:
Evaluate(WorkflowExecutable executable)returns a non-persistedWorkflowTriggerPreflightOutcome; the compatibility projectionExtract(WorkflowExecutable executable)remains available. - Usage: walks the pinned executable's node tree, selects compiler-marked start-trigger nodes, and evaluates every registered
IActivityTriggerStimulusProviderstrategy exactly once per node. Exactly one provider must recognize each node. Zero claims, multiple claims, a blank provider id, invalid descriptor identity, or duplicate deterministic binding ids fail withWorkflowTriggerPreflightExceptionbefore index mutation. One recognized provider returning zero descriptors recordsIntentionallyNonStartingand yields no binding. Public preflight/index contracts declare their typed failures with XML<exception>documentation. - Default implementation:
WorkflowTriggerBindingExtractor.
- Kind: Replacement (one indexer writes the trigger index for a published artifact).
- Signature:
IndexAsync(WorkflowExecutable executable, CancellationToken cancellationToken = default). - Usage: invoked inside the publish flow; completes preflight for the whole artifact, runs every
IWorkflowTriggerIndexValidatorover the completed binding set, then replaces the artifact's prior bindings (delete-by-artifact then write). All semantic failures occur before mutation. After the write succeeds — before returning — it notifies everyIWorkflowTriggerIndexObserverwith the artifact's new bindings. Store or observer failures after mutation begins still propagate; this seam does not promise publication-wide transactionality. - Default implementation:
WorkflowTriggerIndexer.
- Kind: Contribution (fan-in; enumerable). Register with
services.TryAddEnumerable(ServiceDescriptor.Singleton<IWorkflowTriggerIndexValidator, MyValidator>()); the indexer resolvesIEnumerable<IWorkflowTriggerIndexValidator>. - Signature:
ValidateAsync(WorkflowTriggerIndexSnapshot snapshot, CancellationToken ct = default)— the snapshot carriesArtifactId+ the artifact's extracted, about-to-be-writtenIReadOnlyCollection<WorkflowTriggerBinding>. - Usage: PRE-write validation (the pre-write counterpart of
IWorkflowTriggerIndexObserver, issue #592 item 2) so a stimulus family can enforce publish-time constraints over the index without the indexer knowing any stimulus type. Called after extraction, before delete-and-resave. Failure policy: a throw fails the publish with the durable index untouched — no rollback needed, and a bad publish can never poison the store for later publishes or startup. Keep family-specific constraints in the owning module: do NOT enforce cross-definition stimulus uniqueness generically — for most stimulus types (e.g. two definitions on one Timer cron) shared identity is legitimate fan-out. - Default implementation: none (an unvalidated index is valid).
HttpEndpointRoutingUniquenessValidator(inElsa.Workflows.Runtime.Http) is the shipped consumer — HTTP(template, method)cross-definition uniqueness.
- Kind: Contribution (fan-in; enumerable). Register with
services.TryAddEnumerable(ServiceDescriptor.Scoped<IWorkflowTriggerIndexObserver, MyObserver>())(or Singleton); the indexer resolvesIEnumerable<IWorkflowTriggerIndexObserver>. - Signature:
OnTriggersIndexedAsync(WorkflowTriggerIndexSnapshot snapshot, CancellationToken ct = default)— the snapshot carriesArtifactId+ the artifact's newIReadOnlyCollection<WorkflowTriggerBinding>. - Usage: post-index notification so a projection derived from the trigger index (e.g. the per-shell HTTP route table) refreshes as an atomic part of the publish, without the indexer depending on any consumer. Called after delete-and-resave, before
IndexAsyncreturns. Failure policy: exceptions are NOT swallowed — an observer that throws fails the publish (same rule as an unindexed trigger). Keep observer work idempotent so a retried publish converges. - Default implementation: none (an unobserved index is valid).
RouteTableTriggerIndexObserver(inElsa.Workflows.Runtime.Http) is the shipped consumer.
- Kind: Contribution (fan-in; enumerable). Register with
services.TryAddEnumerable(ServiceDescriptor.Singleton<IBookmarkLifecycleObserver, MyObserver>()); fanned in byBookmarkLifecycleNotifier, which the two commit sites resolve. - Signature:
OnBookmarkCreatedAsync(BookmarkState bookmark, CancellationToken ct = default),OnBookmarkConsumedAsync(BookmarkState bookmark, CancellationToken ct = default)— each carries the committedBookmarkState(incl.Metadata). - Usage: post-commit notification so a projection derived from waiting bookmarks (e.g. the per-shell HTTP route table for mid-flow endpoints) refreshes as bookmarks come and go, without the runtime depending on any consumer.
OnBookmarkConsumedAsyncfires AFTER the bookmark-consumed checkpoint commits (BookmarkConsumptionCheckpointService, inline).OnBookmarkCreatedAsyncfires AFTER the bookmark-created checkpoint commits from whichever commit site ran: because the drainer dispatchesCreateBookmarkthrough the ADR-0029 activity pipeline, the created notification fires fromRuntimeActivityCheckpointMiddleware(the pipeline Checkpoint slot) after it commits a stagedBookmarkCreatedcheckpoint;WorkflowCreateBookmarkSchedulerWorkHandler's directHandleAsyncalso notifies after its inline commit for the non-pipeline/unit path. The two paths are mutually exclusive per dispatch (the drainer uses the pipeline), and observer work is a full re-projection anyway, so a redundant refresh would be harmless. Failure policy (opposite ofIWorkflowTriggerIndexObserver): this fires on the RUN path — an observer exception is caught and logged byBookmarkLifecycleNotifierand NEVER faults the run (a stale route simply 404s until the next refresh). Keep observer work idempotent and cheap; a throw is swallowed. - Default implementation: none (an unobserved bookmark lifecycle is valid).
RouteTableBookmarkObserver(inElsa.Workflows.Runtime.Http, spec 089 D Worker B / T010) is the shipped consumer.
- Kind: Replacement (narrow best-effort dedup for the stimulus START path, Condition A).
- Signature:
TryBeginStart(string idempotencyKey). - Usage: when the router is given an
idempotencyKey, a duplicate at-least-once delivery under the same key does not double-start. The default is in-process and best-effort (not a durable cross-node ledger); when no key is supplied the start path is plainly at-least-once and may double-start. Hosts needing restart-durable start-once semantics replace this contract. - Default implementation:
InMemoryStimulusStartDeduplicator.
- Kind: Replacement (one routing spine turns an external stimulus into starts and/or resumes).
- Signature:
RouteAsync(StimulusDispatchRequest request, CancellationToken cancellationToken = default). - Usage: the E3-1/E3-5 spine. Snapshots the cross-execution resume set (via
IGlobalBookmarkStimulusLookup) before starting new instances, starts matching published triggers (viaIWorkflowStartDispatcher, deduped byIStimulusStartDeduplicatorwhen an idempotency key is present), and resumes each waiting instance (viaIBookmarkResumeDispatcher). All dispatch routes through the actor mailbox — the single-writer invariant is preserved. Correlation scope is a passive threaded metadata value. Stimulus-input delivery (spec 089 A):StimulusDispatchRequest.Inputreaches BOTH sides — resumes receive it as the resume input (as before), and starts receive it on the first-classWorkflowExecutionStartDispatchRequest.StimulusInputfield, seeded onto a reserved durable channel (RuntimeMetadataKeys.StimulusInputName, value-id prefixstimulus:) and surfaced to activities viaIExecutionExpressionState.StimulusInput. Deliberately NOT the workflow-inputs bag: the payload can neither collide with an author-declared input nor be forged through the execute API's inputs map. Trigger-node identity (spec 089 D): the matched binding'sExecutableNodeIdrides an analogous reserved channel —StimulusRouterforwards it on the first-classWorkflowExecutionStartDispatchRequest.TriggerNodeIdfield, seeded onto a reserved durable channel (RuntimeMetadataKeys.TriggerNodeId, value-id prefixtrigger:) and surfaced viaIExecutionExpressionState.TriggerNodeId, so a mid-flow-capable activity (e.g.HttpEndpoint) can tell whether it is the node that triggered this run. Null on direct (non-trigger) starts and resume-only paths; same collision/spoof-proofing. Single claimant lookup (#592 item 7):StimulusDispatchRequest.MatchedTriggerBindingslets a caller that already fetched the (type, hash) match set — e.g. the HTTP endpoint middleware, for its ambiguity guard + per-endpoint options — hand it to the router, which reuses it on the start path instead of issuing its own identicalListByStimulusAsync. Null (the default) means the router fetches the set itself. The supplied set must be the complete match for the (type, hash); a partial set under-starts. Dispatch-options passthrough (spec 089 E-D4):StimulusDispatchRequest.DispatchOptions(optionalWorkflowExecutionCommandDispatchOptions?, a live reference excluded fromBuildDispatchMetadataso it never enters the durable envelope) is forwarded to BOTH fan-out sites — starts (IWorkflowStartDispatcher) and resumes (IBookmarkResumeDispatcher) — so one request scope's ambient services serve every outcome of one HTTP request (a start, a resume, or both). Null for non-sync callers (unchanged). - Default implementation:
StimulusRouter.
- Kind: Query surface (read-only active-scope output lookup used by resolvers and resolution contexts).
- Signature:
TryGet(ActiveActivityOutputKey key, out ActiveActivityOutput output),GetActivityOutputs(...). - Usage: exposes active execution outputs without granting mutation rights to consumers that only materialize runtime input bindings.
- Default implementation:
InMemoryRuntimeActivityOutputRegister(throughIRuntimeActivityOutputRegister).
- Kind: Replacement (one active-scope output register owns execution-local activity output lookup for a runtime composition).
- Signature: inherits
IRuntimeActivityOutputReader; addsSet(ActiveActivityOutput output),ClearActivityOutputs(...). - Usage: stores activity outputs by
WorkflowExecutionId,ActivityExecutionId, and output name while they remain in active execution scope. This is not durable continuation state. - Default implementation:
InMemoryRuntimeActivityOutputRegister(intra-domain default, contract/default for current slice).
- Kind: Replacement (one resolver owns runtime input binding materialization rules for a runtime composition).
- Signature:
Resolve(RuntimeInputBinding binding, RuntimeInputBindingResolutionContext context). - Usage: resolves literal, reference, durable-value, and active activity-output bindings without loading authored data links or history snapshots. Expression bindings remain declarations for expression middleware.
- Default implementation:
RuntimeInputBindingResolver(intra-domain default).
- Kind: Replacement (one validator owns executable binding diagnostics for a runtime composition).
- Signature:
Validate(RuntimeInputBinding binding, RuntimeInputBindingValidationContext context). - Usage: reports artifact/build diagnostics for output references that cross suspension boundaries or are ambiguous in loop/parallel scopes.
- Default implementation:
RuntimeInputBindingValidator(intra-domain default).
- Kind: Replacement (one materializer owns conversion from executable input bindings to activity runtime arguments for a composition).
- Signature:
MaterializeInputsAsync(ExecutableNode node, IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default)andMaterializeInputsAsync(ExecutableNode node, RuntimeInputBindingResolutionContext resolutionContext, CancellationToken cancellationToken = default). - Usage: constructs activity input arguments and memory values from runtime-owned executable node bindings. Supports literal, durable-value, active-output, and reference bindings (all requiring
typeNamemetadata), plusExpressionbindings — evaluated through the registeredIExpressionEvaluator(e.g. JavaScript/Liquid) using the resolution context'sServiceProvider. Expression evaluation requires a service provider; the literal-only convenience overload passes one through for resume paths. Expressions resolve workflow variables, workflow inputs, and prior activity outputs supplied onRuntimeInputBindingResolutionContext(WorkflowVariables,WorkflowInputs,ActivityOutputValues): the materialization-timeIExpressionExecutionContextimplementsIMaterializationExpressionState, which language pre-processors (e.g.MaterializationAccessorsPreProcessor) read to surfacevariables/input/outputaccessors without a live workflow execution context. - Default implementation:
RuntimeActivityInputMaterializer.
- Kind: Bridge (carries workflow-scoped values from
RuntimeInputBindingResolutionContextto language pre-processors during activity-input materialization). - Signature:
WorkflowVariables,WorkflowInputs,ActivityOutputValues(name → value). - Usage: implemented by the materialization-time
IExpressionExecutionContextso expression-language pre-processors can resolve variable/input/output references before the activity's real execution context exists. Populate the snapshots on the resolution context from the durable values captured for the execution:RuntimeInputBindingStateProjection.ProjectWorkflowVariables/ProjectWorkflowInputs/ProjectActivityOutputValuesrebuild thevariables.*/input.*/output.*snapshots. Workflow variables and inputs become durable values viaRuntimeWorkflowStateSeed, seeded at theWorkflowStartedcheckpoint and tagged with theruntime.variableName/runtime.inputNamemetadata keys. The start entry points (ExecuteWorkflowRequestHandler,StartWorkflowTestRunRequestHandler) populate the seed'sVariableswith authored workflow variable defaults projected off the compiled executable's root structure (RuntimeVariableScopeFactory.ProjectDeclaredVariableDefaultsByName); caller-suppliedInputsare a deferred API-surface change (#286). - Default implementation: private materialization context in
RuntimeActivityInputMaterializer; consumed byMaterializationAccessorsPreProcessor(Elsa.Workflows.Runtime.JavaScript).
- Kind: Bridge (carries live execution-time workflow state — identity, inputs, variables, prior outputs — to language pre/post-processors during activity execution). Execution-time counterpart to
IMaterializationExpressionState(ADR 0030). - Signature:
WorkflowInstanceId,CorrelationId,WorkflowName,WorkflowDefinitionId,WorkflowDefinitionVersionId,WorkflowDefinitionVersion,WorkflowInputs,StimulusInput,TriggerNodeId,ResumeInput,WorkflowVariables,ActivityOutputValues. - Usage: implemented by the execution-time
IExpressionExecutionContext(SimpleActivityExecutionContext) so expression-language pre/post-processors resolve execution-time identity functions, named pascalized accessors, execution-time output accessors, and JavaScript variable write-back without a DI-registered live workflow execution context (ADR 0030 D1; retiresIWorkflowExecutionContext). Populated byWorkflowInvokeActivitySchedulerWorkHandler: identity fromWorkflowExecutionState+ the pinnedWorkflowExecutableIdentity;WorkflowVariables/WorkflowInputs/ActivityOutputValuesfrom the durable-value projections (RuntimeInputBindingStateProjection). Variable writes route through the visibleVariableScope(IScopedVariableProvider) and fold into the checkpoint-commit durable-value write-back (BuildWorkflowScopeWriteBackChanges) — no second persistence route.StimulusInput/TriggerNodeIdcome from the same durable-value projection set;ResumeInputis populated only on the resume path (WorkflowResumeBookmarkSchedulerWorkHandlerstashes the resume dispatch's input onto the carrier, spec 089 D) as a live per-invocation value — never durable state — so a context-shaped[ResumeTarget]reads the resuming request payload while keeping fullSet/output access; null on start/invoke/parent-completion. A narrow marker, not a general transient-properties bag (ADR 0030 Q3); Design-free (§E2.2/§E2.6). - Default implementation:
SimpleActivityExecutionContext; consumed by the re-pointed JavaScript pre/post-processors (WorkflowFunctionsPreProcessor,WorkflowInputFunctionsPreProcessor,VariableFunctionsPreProcessor,ActivityOutputFunctionsPreProcessor,CopyVariablesToWorkflowContext) in Elsa.Workflows.Runtime.JavaScript. TheJavaScriptWorkflowsRuntimeFeatureregistration is covered by a resolve-and-evaluate guardrail test (ADR 0030 D4).
- Kind: Replacement (one policy decides which runtime observability payloads may be captured for a runtime composition).
- Signature:
Decide(RuntimePayloadCaptureRequest request). - Usage: controls whether history, diagnostics, incidents, values, and input/output observations capture no payload, metadata only, or full payload. Continuation state does not read these observability payloads. The default excludes sensitive values and omits workflow/activity input and output snapshots.
- Default implementation:
DefaultRuntimePayloadCapturePolicy(intra-domain default).
- Kind: Replacement (one provider owns workflow-execution mailbox activation, routing, and passivation for a runtime composition).
- Signature:
Capabilities,GetAgentAsync(WorkflowExecutionActorActivationRequest request, CancellationToken cancellationToken = default),PassivateAsync(WorkflowExecutionActorPassivationRequest request, CancellationToken cancellationToken = default). - Usage: provider implementations enforce one active mailbox/agent per
WorkflowExecutionId. Commands are delivered throughWorkflowExecutionCommandEnvelope, which carries command identity, workflow execution ID, idempotency key, optional sequence, delivery mode, and metadata. Actor frameworks are provider choices; checkpoint state remains the source of truth. - Default implementation:
InProcessWorkflowExecutionActorProvider(single-node actor-like mailbox; no distributed placement or actor framework dependency). - Alternative implementation:
DistributedWorkflowExecutionActorProvider(opt-in leafElsa.Workflows.Runtime.Distributed, W20/E3-3) — clustered placement/routing over the in-process provider: claims per-execution placement, returns the local in-process actor when this node owns the execution, or aForwardingWorkflowExecutionActor(durable-transport routing stub,Deferredresult) when another node owns it. Placement is best-effort routing; W5 fencing at checkpoint commit is the authoritative double-execution guard. Enable with theWorkflowsRuntimeDistributedshell feature. See the Distributed placement and transport section below.
Leaf-owned contracts for clustered workflow-execution placement and cross-node command routing. Per §2.7 these live entirely in the provider leaf; Elsa.Workflows.Runtime.Core gains zero references to them. The leaf consumes the W5 single-writer fencing seam (IRuntimeExecutionOwnershipService) unchanged — placement decides which node drains (routing), fencing decides whether a write commits (safety).
-
- Kind: Replacement (one store owns per-execution placement lease records for a distributed composition).
- Signature:
TryClaimAsync,FindAsync,ReleaseAsync,ListAsync(compare-and-swap on placement token; claim doubles as renew). - Usage: the CAS claim/renew primitive under placement ownership. Claiming an unowned or expired placement issues a strictly greater placement token; a claim against a live foreign lease fails without mutation.
- Default implementation:
InMemoryExecutionPlacementStore(single-process/two-node-harness default).GroundworkExecutionPlacementStore(durableIDocumentStore-backed bridge, W27 — opt-in leafElsa.Workflows.Runtime.Distributed.Persistence.Groundwork, swapped in by theWorkflowsRuntimeDistributedGroundworkPersistencefeature; exact cross-node CAS via the provider's ExpectedVersion contract, including create-only first claims).
-
- Kind: Replacement (one service owns this node's placement acquisition/renewal/release policy).
- Signature:
NodeId,TryClaimAsync,FindOwnerAsync,ListOwnedAsync,ReleaseAsync. - Usage: wraps the store with this node's identity, lease duration, and
TimeProviderso all lease timing is deterministic and options-driven. - Default implementation:
ExecutionPlacementService.
-
- Kind: Replacement (one transport owns the durable cross-node command inbox for a distributed composition).
- Signature:
SendAsync,LeaseAsync,AckAsync,ListPendingExecutionIdsAsync,CountPendingAsync(ack-based lease/visibility, at-least-once). - Usage: commands for an execution owned by another node are durably enqueued, then leased/acked by the owning node's pump. A lease hides an item from other nodes until acked or expired; only the live lease holder may ack, so a superseded node's ack is refused and the item is re-driven on failover. Wire shape is frozen by the committed v1 golden fixture (§E6 kind
executionCommandTransport). - Default implementation:
InMemoryExecutionCommandTransport(single-process/two-node-harness default).GroundworkExecutionCommandTransport(durableIDocumentStore-backed bridge, W27 — same leaf/feature as the placement store; persists the frozen v1executionCommandTransportitem shape, store-enforced unique per-execution sequences, version-guarded lease/ack CAS).
-
Kind: Registered recurring task (
IRecurringTask; one per node, DependsOn Tasks). -
Usage: each bounded sweep renews the placements this node holds, then discovers executions with visible transport backlog, claims any it can own, leases their commands, dispatches each to the local actor, and acks on a delivered outcome. Deferred/Rejected dispatches stay leased so lease expiry re-drives them (the failover loop). All cadence/bounds come from
ExecutionPlacementPumpOptionsevaluated againstTimeProvider; a failing sweep is logged, never rethrown, and widens the interval geometrically. -
Kind: Replacement (one generator owns runtime command-dispatch IDs for a runtime composition).
-
Signature:
NewWorkflowExecutionId(),NewWorkflowExecutionCommandId(),NewWorkflowExecutionCommandEnvelopeId(),NewActivityExecutionId(). -
Usage: provides runtime-owned identifiers for workflow execution start dispatch and concrete activity executions without leaking API, persistence, or provider-specific identity generation into command construction.
-
Default implementation:
ShortRuntimeExecutionIdGenerator.
- Kind: Replacement (one dispatcher owns conversion from executable artifact start requests into workflow execution agent commands).
- Signature:
DispatchAsync(WorkflowExecutionStartDispatchRequest request, WorkflowExecutionCommandDispatchOptions? dispatchOptions = null, CancellationToken cancellationToken = default)(plusDispatchTransientAsyncwith the same optional options param). (spec 089 E) - Usage: loads the runtime-owned executable artifact, pins its exact identity in a
WorkflowExecutionCommandKind.Startpayload, activates the workflow execution agent, and enqueues the command envelope (threadingdispatchOptions ?? Defaultintoagent.EnqueueAsync). It does not execute activities inline. Ambient-services passthrough (spec 089 E-D4/FR-019):dispatchOptions.AmbientServicesreaches the inline drain's activity context (spec-069 chain), so a request-affineWriteHttpResponsecan write the live sync response. The options are a live reference — NOT durable state; they never serialize into the envelope, andForwardingWorkflowExecutionActorDROPS them (returnsDeferred), so ambient services never cross the process boundary (FR-021 invariant, tested). - Default implementation:
WorkflowExecutionStartDispatcher.
- Kind: Replacement (one processor decides what an accepted workflow-execution command does inside the active actor mailbox).
- Signature:
ProcessAsync(WorkflowExecutionCommandEnvelope envelope, CancellationToken cancellationToken = default). - Usage: invoked by the in-process agent after dispatch metadata has been accepted and before the idempotency key is marked processed. The processor runs under the actor mailbox's single-writer boundary.
- Default implementation:
WorkflowSchedulerCommandRouter(records accepted commands as scheduler work, applies the scheduler drain policy, then delegates command-triggered draining toIWorkflowDrainOrchestrator; activity execution remains handler/provider behavior, not command acceptance behavior).
- Kind: Replacement (one coordinator owns command-triggered workflow execution drain orchestration for a runtime composition).
- Signature:
DrainAsync(WorkflowExecutionCommandEnvelope envelope, RuntimeSchedulerDrainRequest request, CancellationToken cancellationToken = default). - Usage: bridges the accepted command boundary to scheduler draining after scheduler work is recorded and the drain policy requests immediate advancement. The default coordinator drains scheduler work, processes deliverable
RuntimePostCommitIntentKinds.EnqueueSchedulerWorkoutbox items for the same workflow execution, and repeats scheduler draining until scheduler-intent delivery quiesces, a pause/fault stops scheduler draining, or the bounded cycle guard is reached. Checkpoint commit remains the durability boundary: commits record post-commit work, and the coordinator only delivers it after the commit path succeeds.WorkflowDrainOrchestratorOptionsnames the cycle and outbox batch limits; cycle-cap exhaustion throwsDrainCycleLimitExceededException. - Default implementation:
WorkflowDrainOrchestrator.
- Kind: Replacement (one service owns single-writer fencing — lease acquisition, heartbeat, release, and stale-writer rejection — for a runtime composition).
- Signature:
AcquireAsync(string workflowExecutionId, ...),HeartbeatAsync(RuntimeExecutionLease lease, ...),ReleaseAsync(RuntimeExecutionLease lease, ...),EnsureCurrentAsync(string workflowExecutionId, long fencingToken, ...). - Usage: enforces RT-2 single-writer ownership.
WorkflowDrainOrchestratoracquires a lease at the start of a drain, pushes it ontoIRuntimeExecutionOwnershipContextAccessor, and releases it in afinally(so a crash leaves the lease persisted for the recovery scanner to detect — closing W2's post-dequeue/pre-commit window).RuntimeCheckpointCommittercallsEnsureCurrentAsyncat the single checkpoint-commit funnel and throwsRuntimeStaleFencingTokenExceptionwhen the presented fencing token is not the current one (equality is the only pass; tokens are strictly monotonic and never reused across release). Ownership state is backed byIExecutionLivenessStateStore; the leaseExpiresAtreuses the recovery scanner's existing lease-timeout honoring rather than a parallel knob. - Default implementation:
RuntimeExecutionOwnershipService(operational-state-backed, monotonic fencing token preserved across release).
- Kind: Replacement (one accessor owns the ambient current-lease scope for a runtime composition).
- Signature:
RuntimeExecutionLease? Current { get; },Push(RuntimeExecutionLease lease) : IDisposable. - Usage: an AsyncLocal push/pop scope that carries the active drain's lease from
IWorkflowDrainOrchestratordown toRuntimeCheckpointCommitterwithout threading it through every command/handler signature. It is a runtime-internal ambient accessor, not the ADR-0029-discouraged pipeline-context ambient. (This is a deliberately retained runtime-internal ambient — distinct from the pipeline-context/ambient-services service locators RT-7 removed from the drain path, whose services now flow explicitly viaRuntimePipelineWorkspace.AmbientServices.) - Default implementation:
AsyncLocalRuntimeExecutionOwnershipContextAccessor.
- Kind: Replacement (one queue owns recorded scheduler work for a runtime composition).
- Signature:
EnqueueAsync(RuntimeSchedulerWorkItem workItem, ...),ListAsync(RuntimeSchedulerWorkQuery query, ...),DequeueAsync(string workflowExecutionId, ...),ListPendingWorkflowExecutionIdsAsync(int limit, ...). - Usage: stores scheduler work by
WorkflowExecutionIdafter an execution agent accepts a command envelope. The queue preserves per-workflow insertion order and is idempotent by scheduler work item ID within each workflow execution.ListPendingWorkflowExecutionIdsAsyncreturns the distinct execution ids with queued work, up tolimit, so a resumption sweep can discover durable backlog after a restart when nothing else knows the interrupted execution ids. Draining and activity execution remain separate scheduler behavior. - Default implementation:
InMemoryWorkflowSchedulerWorkQueue(single-node in-memory default for the current runtime slice).GroundworkWorkflowSchedulerWorkQueue(durableIDocumentStore-backed bridge; swapped in byAddGroundworkRuntimeStoresso scheduler work survives a process crash — see docs/runtime-durable-resumption.md).
- Kind: Replacement (one store owns durable timers for a runtime composition).
- Signature:
SaveAsync(DurableTimer timer, ...),FindAsync(string workflowExecutionId, string timerId, ...),DeleteAsync(string workflowExecutionId, string timerId, ...),ListDueAsync(DateTimeOffset now, int limit, ...). - Usage: persists
DurableTimerrecords (a due-time-indexed document kind,durableTimer) keyed by(WorkflowExecutionId, TimerId).SaveAsyncis a deterministic upsert so a pre-commit crash re-executing the owning activity re-writes the same timer rather than duplicating it.ListDueAsyncreturns timers withDueTime <= now, ordered by(DueTime, TimerId), capped atlimit; the durable timer pump (DurableTimerPumpTask) drains this and fires each due timer throughIBookmarkResumeDispatcher. The pump owns idempotency: it deletes a timer onDispatched/Duplicate(the resume is durably enqueued intoIWorkflowSchedulerWorkQueuebefore the dispatcher returns — seeWorkflowSchedulerCommandRouter.ProcessAsync— so deletion cannot lose the resume), and treats a past-graceNotFoundas an already-consumed bookmark. - Default implementation:
InMemoryDurableTimerStore(single-node in-memory default; Delay works but is not restart-durable without a durable store).GroundworkDurableTimerStore(durableIDocumentStore-backed bridge; swapped in byAddGroundworkRuntimeStores). - Follow-ups (W8):
- Native due-time range index. Groundwork is equality-index only this wave, so
ListDueAsyncloads the whole timer partition (equality query on a constant collection key) and filters/ordersDueTimein memory.MaxTimersPerTickbounds the dispatch burst, not the load. A native range/due-time index in Groundwork is the scale follow-up. - Timer/Cron start triggers (recurring schedules that start a workflow) ship via a dedicated recurring-trigger schedule store + pump (see
IRecurringTriggerScheduleStore/IRecurringTriggerScheduleProviderbelow and theWorkflowsRuntimeRecurringTriggersfeature), not thedurableTimerstore. Rationale: the durable-timer pump resumes an existing execution (it has aWorkflowExecutionId); a start trigger has none, so it needs the trigger/stimulus router (W7) to start a workflow. ThedurableTimerkind therefore remains resume-only. - Atomic timer registration (Option B). Delay registers its timer activity-side, strictly before the bookmark (so "bookmark committed, timer missing" is structurally excluded). A fully atomic timer==bookmark lifecycle via a post-commit
RegisterDurableTimerintent (IRuntimePostCommitIntentDispatcher) is the alternative if orphaned timers ever prove noisy.
- Native due-time range index. Groundwork is equality-index only this wave, so
- Kind: Replacement (one scheduler owns durable-timer registration for a runtime composition).
- Signature:
ScheduleAsync(DurableTimer timer, ...). - Usage: thin activity-facing wrapper over
IDurableTimerStore.SaveAsync. TheDelayactivity builds theDurableTimer(derivingDueTimefrom the injectedTimeProvider) and calls this to write its timer before creating the matching bookmark. - Default implementation:
DurableTimerScheduler(registered by theWorkflowsRuntimeSchedulingfeature).
- Kind: Replacement (one store owns recurring-start schedules for a runtime composition).
- Signature:
SaveAsync(RecurringTriggerSchedule schedule, ...),ListDueAsync(DateTimeOffset asOf, int limit, ...),FindAsync(string scheduleId, ...),TryAdvanceAsync(string scheduleId, DateTimeOffset expectedNextOccurrence, DateTimeOffset newNextOccurrence, ...),DeleteByArtifactAsync(string artifactId, ...),DeleteAsync(string scheduleId, ...). - Usage: persists
RecurringTriggerSchedulerecords (a next-occurrence-indexed document kind,recurringTriggerSchedule) for Timer/Cron start triggers — the recurring-start counterpart toIDurableTimerStore(which is resume-only).SaveAsyncis an idempotent upsert keyed byScheduleId; republishing an artifact replaces its schedules viaDeleteByArtifactAsync+ re-save, mirroring the trigger index. The recurring-trigger pump (RecurringTriggerPumpTask,WorkflowsRuntimeRecurringTriggersfeature) drainsListDueAsyncand, for each due schedule, claims the occurrence withTryAdvanceAsync(compare-and-swap onNextOccurrence) before firing the trigger stimulus throughIStimulusRouter. Missed-occurrence policy: on pump wake after downtime a schedule fires at most once and advances straight to the next future occurrence — the backlog is never replayed. Cluster-safety hook (W20):TryAdvanceAsyncis the compare-and-swap a future clustered store keeps so at most one node fires an occurrence, without changing the pump. - Default implementation:
InMemoryRecurringTriggerScheduleStore(single-node in-memory default; start triggers work but are not restart-durable without a durable store).GroundworkRecurringTriggerScheduleStore(durableIDocumentStore-backed bridge; swapped in byAddGroundworkRuntimeStores).
- Kind: Strategy set (context-selected, exact-one owner per recurring-trigger node; not a contributor fan-in).
- Signature: additive stable nonblank
ProviderId;RecurringScheduleDescriptor? Describe(ExecutableNode node); - Usage: the recurring-schedule sibling of
IActivityTriggerStimulusProvider. At publish time the schedule indexer asks every provider to describe a node; a provider returns the node's recurrence spec (interval / cron expression → next occurrence) when it recognizes the activity type, ornull("not mine"). Multiple claims fail with contextual provider ids rather than selecting by registration order.RecurringTriggerScheduleIndexermaterializes the complete Timer/Cron schedule candidate set before it invokes the inner trigger indexer. Invalid expressions, calculator failures, and exhausted Cron schedules are wrapped in contextualWorkflowTriggerPreflightExceptionfailures before either schedule or binding replacement begins. A recurring-trigger activity contributes both seams: one trigger binding and one materialized schedule. Providers read only the pinned publishedExecutableNode. - Register:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IRecurringTriggerScheduleProvider, MyProvider>()).
Known implementations (shipped):
Elsa.Activities.Scheduling—TimerRecurringScheduleProvider/CronRecurringScheduleProvider(cross-domain — describe theTimer(fixed interval) andCron(cron expression, via Cronos) recurring start schedules).
- Kind: Replacement (one store owns split continuation state for concrete activity executions in a runtime composition).
- Signature:
SaveAsync(ActivityExecutionState state, ...),FindAsync(string workflowExecutionId, string activityExecutionId, ...),ListAsync(string workflowExecutionId, ...). - Usage: stores
ActivityExecutionStatekeyed byWorkflowExecutionIdand durableActivityExecutionId.SaveAsyncis an upsert for future lifecycle transitions. The default scheduler uses it to recordScheduledstate whenScheduleActivitywork is drained, but it does not overwrite an existing activity execution state when replaying the same schedule work. It does not invoke activities, store authored workflow documents, or project diagnostics/history. - Default implementation:
InMemoryActivityExecutionStateStore(single-node in-memory default for the current runtime slice).
- Kind: Replacement query surface (one store owns committed inspection projections for concrete activity executions in a runtime composition).
- Signature:
FindAsync(string workflowExecutionId, string activityExecutionId, ...),ListSummariesAsync(string workflowExecutionId, ...). - Usage: reads runtime-owned inspection evidence keyed by concrete activity execution identity. Consumers use this store for lightweight per-instance activity execution summaries and selected execution detail without loading authored workflow documents.
- Default implementation:
InMemoryActivityExecutionInspectionStore(single-node in-memory default for the current runtime slice). - Known provider implementations:
Elsa.Persistence.Groundwork—GroundworkActivityExecutionInspectionStore(cross-domain persistence provider replacement).
- Kind: Replacement command surface (one writer owns committed inspection projection upserts for concrete activity executions in a runtime composition).
- Signature:
SaveAsync(ActivityExecutionInspectionProjection projection, ...). - Usage: writes runtime-owned inspection evidence from accepted checkpoint commits through the activity-execution-inspection lane, so inspection evidence does not get ahead of lifecycle state. The command surface is split from
IActivityExecutionInspectionStoreto preserve command/query separation. - Default implementation:
InMemoryActivityExecutionInspectionStore(single-node in-memory default for the current runtime slice). - Known provider implementations:
Elsa.Persistence.Groundwork—GroundworkActivityExecutionInspectionStore(cross-domain persistence provider replacement).
- Kind: Replacement (one accumulator assembles checkpoint-scoped activity execution inspection projections for a runtime composition).
- Signature:
BuildProjectionAsync(ActivityExecutionState state, string checkpointId, DateTimeOffset committedAt, ...). - Usage: merges lifecycle state with committed outcome, bookmark, incident, value-snapshot, provenance, checkpoint, and metadata evidence before the checkpoint writer persists the inspection projection. Provider implementations can replace this when a runtime composition needs different projection merge/enrichment behavior while preserving the checkpoint lane contract.
- Default implementation:
RuntimeActivityExecutionInspectionAccumulator(intra-domain default).
- Kind: Replacement (one store owns split continuation state for durable bookmark resume handles in a runtime composition).
- Signature:
SaveAsync(BookmarkState state, ...),DeleteAsync(string workflowExecutionId, string bookmarkId, ...),FindAsync(string workflowExecutionId, string bookmarkId, ...),ListAsync(string workflowExecutionId, ...). - Usage: stores
BookmarkStatekeyed byWorkflowExecutionIdandBookmarkId. The in-memory checkpoint writer projects bookmark upserts and deletes from accepted checkpoint commits into this store. Stimulus lookup indexes and resume dispatch behavior are separate runtime surfaces and are not part of this store boundary. - Default implementation:
InMemoryBookmarkStateStore(single-node in-memory default for the current runtime slice).
- Kind: Replacement (one store owns split continuation state for declared durable runtime values in a runtime composition).
- Signature:
SaveAsync(DurableValueState state, ...),DeleteAsync(string workflowExecutionId, string durableValueId, ...),FindAsync(string workflowExecutionId, string durableValueId, ...),ListAsync(string workflowExecutionId, ...). - Usage: stores
DurableValueStatekeyed byWorkflowExecutionIdandDurableValueId. The in-memory checkpoint writer projects durable value upserts and deletes from accepted checkpoint commits into this store. Storage drivers, capture middleware, and history snapshots are separate runtime surfaces. - Default implementation:
InMemoryDurableValueStateStore(single-node in-memory default for the current runtime slice).
- Kind: Replacement (one store owns split continuation state for execution-affecting incidents in a runtime composition).
- Signature:
TryAddAsync(IncidentState state, ...),SaveAsync(IncidentState state, ...),FindAsync(string workflowExecutionId, string incidentId, ...),ListAsync(string workflowExecutionId, ...),ListBlockingAsync(string workflowExecutionId, ...). - Usage: stores
IncidentStatekeyed byWorkflowExecutionIdandIncidentId. The in-memory checkpoint writer projects incident appends as insert-only changes and incident upserts as replacements from accepted checkpoint commits into this store. Incident history projections, diagnostic payloads, retry, compensation, and intervention behavior are separate runtime surfaces. - Default implementation:
InMemoryIncidentStateStore(single-node in-memory default for the current runtime slice).
- Kind: Replacement (one store owns split continuation state for runtime operational coordination in a runtime composition).
- Signature:
SaveAsync(ExecutionLivenessState state, ...),FindAsync(string workflowExecutionId, string operationalStateId, ...),ListAsync(string workflowExecutionId, ...),ListAllAsync(...). - Usage: stores
ExecutionLivenessStatekeyed byWorkflowExecutionIdandOperationalStateId. The in-memory checkpoint writer projects operational state upserts from accepted checkpoint commits into this store. Recovery scanning, outbox delivery processing, domain retry, and actor-provider lease enforcement remain separate runtime surfaces. - Default implementation:
InMemoryExecutionLivenessStateStore(single-node in-memory default for the current runtime slice).
- Kind: Replacement (one store owns the split scheduler continuation-state snapshot in a runtime composition).
- Signature:
SaveAsync(SchedulerState state, ...),FindAsync(string workflowExecutionId, ...),ListAsync(...). - Usage: stores
SchedulerStatekeyed byWorkflowExecutionId. The in-memory checkpoint writer projects scheduler state upserts from accepted checkpoint commits into this store. This is distinct fromIWorkflowSchedulerWorkQueue, which records accepted scheduler work commands before/driving drains. - Default implementation:
InMemorySchedulerStateStore(single-node in-memory default for the current runtime slice).
- Kind: Replacement (one drainer owns deterministic dispatch of queued scheduler work for a runtime composition).
- Signature:
DrainAsync(RuntimeSchedulerDrainRequest request, CancellationToken cancellationToken = default). - Usage: dequeues scheduler work for one workflow execution and dispatches each work item to an
IWorkflowSchedulerWorkHandler. The default drainer stops on the first handler fault and returns per-item drain results. It does not execute activities, write checkpoints, or implement retry. - Default implementation:
WorkflowSchedulerDrainer(contract-only drain boundary for the current runtime slice).
- Kind: Replacement (one gate evaluates whether queued scheduler work may cross named pause boundaries).
- Signature:
EvaluateAsync(RuntimeSchedulerWorkItem workItem, CancellationToken cancellationToken = default). - Usage: maps supported scheduler command kinds to
RuntimePauseDecisionRequestvalues and delegates toIRuntimePauseDecisionProvider. The default drainer peeks the next queued item, consults this gate, and stops without dequeuing when the decision blocks advancement. - Default implementation:
WorkflowSchedulerPauseGate(mapsStartActivity/InvokeActivitytoBeforeActivityExecutionStartandGeneratedEventtoBeforeGeneratorEmission).
- Kind: Replacement (one policy decides whether recorded scheduler work triggers a drain in the runtime composition).
- Signature:
CreateDrainRequest(WorkflowExecutionCommandEnvelope envelope, RuntimeSchedulerWorkItem workItem). - Usage: command processing records scheduler work first, then asks this policy whether to drain. Returning
nulldefers draining. - Default implementation:
ImmediateWorkflowSchedulerDrainPolicy.
- Kind: Contributor (observers consume coordinated drain results produced by workflow execution draining).
- Signature:
OnDrainedAsync(WorkflowExecutionCommandEnvelope envelope, RuntimeSchedulerDrainResult result, CancellationToken cancellationToken = default). - Usage: modules can project one command-triggered coordinated drain outcome into diagnostics or future checkpoint/outbox behavior without making history continuation state. Coordinated results aggregate scheduler work item results across scheduler drain passes, include post-commit outbox delivery counts/results, and expose a stop reason such as quiesced, paused, faulted, or outbox delivery failed.
- Default implementation:
NoopWorkflowSchedulerDrainObserver. - Known implementations (shipped):
BlockingIncidentWorkflowFaultObserver(RT-1a/RT-5 — after a drain turn, if the workflow has one or more blocking incidents and is still non-terminal, commits aWorkflowFaultedcheckpoint that transitions the workflow toFaulted; registered additively viaTryAddEnumerable).
- Kind: Contributor (handlers consume drained scheduler work items).
- Signature:
Name,CanHandle(RuntimeSchedulerWorkItem workItem),HandleAsync(RuntimeSchedulerWorkItem workItem, CancellationToken cancellationToken = default). - Usage: modules can handle specific scheduler command kinds without replacing the drainer. The drainer evaluates ordinary handlers before fallback handlers.
- Default implementations:
WorkflowStartSchedulerWorkHandler(turnsStartwork intoScheduleActivitywork for executable start nodes),WorkflowScheduleActivitySchedulerWorkHandler(recordsScheduledActivityExecutionStateand queuesStartActivitywork for one executable node),WorkflowStartActivitySchedulerWorkHandler(transitions scheduled activity state toRunningand queuesInvokeActivitywork),WorkflowCompleteActivitySchedulerWorkHandler(drains deterministic activity completion work),WorkflowCheckpointSchedulerWorkHandler(commits named checkpoint scheduler work throughRuntimeCheckpointCommitter),MissingActivityInvocationSchedulerWorkHandler(fallback that faultsInvokeActivitywhen no provider is composed),MissingBookmarkResumeSchedulerWorkHandler(fallback that faultsResumeBookmarkwhen no bookmark resume provider is composed),MissingGeneratedEventSchedulerWorkHandler(fallback that faultsGeneratedEventwhen no generated-event provider is composed), andNoopWorkflowSchedulerWorkHandler(fallback that acknowledges drained work that has no required provider-specific handler).
- Kind: Contributor marker (handlers consume drained scheduler work items only after ordinary handlers decline them).
- Signature: inherits
IWorkflowSchedulerWorkHandler. - Usage: modules can register catch-all scheduler work handlers without becoming priority handlers. The default drainer evaluates these handlers after ordinary
IWorkflowSchedulerWorkHandlerregistrations. - Default implementation:
NoopWorkflowSchedulerWorkHandler.
- Kind: Replacement (one store owns runtime executable artifact lookup for a runtime composition).
- Signature:
SaveAsync(WorkflowExecutable executable, ...),FindAsync(string artifactId, ...),ListAsync(...). - Usage: stores and retrieves runtime-owned
WorkflowExecutableartifacts. Publishing writes artifacts through this contract; Runtime execution reads artifacts through this contract and does not load Design-owned workflow state. - Default implementation:
InMemoryWorkflowExecutableStore(intra-domain demo default for the vertical slice; durable persistence remains future provider work).
- Kind: Contributor (workflow runtime pipeline step).
- Signature:
InvokeAsync(WorkflowRuntimePipelineContext context, WorkflowRuntimeMiddlewareDelegate next). - Register: via
WorkflowRuntimePipelineBuilder.Use<TMiddleware>(slotName, order, name). - Usage: registers workflow execution middleware into stable slots from
RuntimeWorkflowPipelineSlots. Plans are inspectable throughBuildPlan(). - Known implementations (shipped): built-in load-state / scheduling / post-commit steps; the
Invokeslot (RuntimeWorkflowInvokeMiddleware) runs the dispatcher-staged handler beforenext, and theCheckpointslot (RuntimeWorkflowCheckpointMiddleware) drains the handler-staged commit list (ADR 0029 Move 2 — slot-invoked handler model).
- Kind: Contributor (activity runtime pipeline step).
- Signature:
InvokeAsync(ActivityRuntimePipelineContext context, ActivityRuntimeMiddlewareDelegate next). - Register: via
ActivityRuntimePipelineBuilder.Use<TMiddleware>(slotName, order, name). - Usage: registers activity execution middleware into stable slots from
RuntimeActivityPipelineSlots. Plans are inspectable throughBuildPlan(). - Known implementations (shipped): built-in load-state / input-evaluation / output-capture / scheduling / post-commit steps; the
Invokeslot (RuntimeActivityInvokeMiddleware) runs the dispatcher-staged handler beforenext, and theCheckpointslot (RuntimeActivityCheckpointMiddleware) drains the handler-staged commit list (ADR 0029 Move 2 — slot-invoked handler model).
- Kind: Contributor opt-in (a migrated scheduler work handler's context-aware overload).
- Signature:
HandleAsync(RuntimeSchedulerWorkItem workItem, IRuntimePipelineContext pipelineContext, CancellationToken). - Usage: ADR 0029 Move 2 slot-invoked handler model. A scheduler work handler additionally implements this interface to run inside the pipeline's
Invokeslot with the per-dispatch context threaded explicitly (no ambient/AsyncLocal accessor). The handler either stages its assembledRuntimeCheckpointCommit(s) onIRuntimePipelineContext.Workspacefor theCheckpointslot to commit in order, one committer call per staged entry (never folded — folding is the coalescing decorators' job), or, for the nested-invoke handlers whose commits must go through a dynamically-resolved provider, commits inline in theInvokeslot and stages nothing. Handlers that have not migrated keep onlyIWorkflowSchedulerWorkHandlerand run their plain path unchanged.RuntimeExecutionPipelineDispatcherstages the selected handler on the workspace and any migrated handler is picked up by a runtime cast. - Staging surface:
RuntimePipelineWorkspace—StageCheckpointCommit(...)/PendingCheckpointCommits(ordered list), thePendingCheckpointCommitsingle-commit convenience, andAmbientServices(the explicit carrier for the drain's request-scoped provider that RT-7 substituted for the removed ambient service locator). - Known implementations (shipped): workflow
Cancel+Checkpoint; activityCreateBookmark,ScheduleActivity,StartActivity(stage), and the nested-invokeInvokeActivity+ParentActivityCompletion(inline-commit, stage nothing).
- Kind: Composition root (host-agnostic runtime registration).
- Usage: RT-4; renamed from
AddWorkflowRuntimeCorewhen the composition root moved to the engine package (ADR 0033). Registers the full hosting-agnostic runtime (stores, scheduler queue/drainer, checkpoint committer, pipelines + built-in middleware, dispatcher, ownership fencing, post-commit outbox) so a worker or test harness can compose and drive a drain without the API feature.WorkflowsRuntimeApiFeaturecomposes it and adds only the API/endpoint concerns. All registrations useTryAdd, so a durable provider overrides any store; the reference stores/handlers are process-global singletons by design (see the composition-root XML docs anddocs/runtime-durable-resumption.md).
- Kind: Shell-scoped policy selector and post-provider decorator.
- Usage: Configure
ModeasImmediate(default/pass-through) orCoalesced, with a positiveMaxSegmentCheckpoints(default 50). The feature implementsIPostConfigureShellServicesso provider packages first replace the runtime stores and coalescing then wraps the selected implementations. Duplicate composition is idempotent. Seedocs/runtime-durable-resumption.mdfor the latency, replay, and cap trade-offs.
- Kind: Declaration surface (activity author contract).
- Signature:
[ResumeTarget("stable-resume-target-id")]on an activity handler method. - Usage: declares the stable resume target ID that compile/publish can place into
WorkflowExecutable.ResumeTargets. Durable bookmarks store the ID, not the C# method name. - Not a runtime callback store — handler method names and delegates are implementation details and are not persisted in
BookmarkState. - Compiler indexing (W8):
WorkflowExecutableCompilernow reflects[ResumeTarget]methods off each node's resolved activity type and indexes them intoWorkflowExecutable.ResumeTargets(previously always empty).Delayis the first suspending activity to exercise this. The map is keyed by the attribute's resume-target ID, so duplicate IDs across nodes fail compilation loudly. - Follow-up (W8) — node-scoped resume targets. Because the key is the attribute ID (matching how the resume resolver and the create-bookmark handler already match), only one instance of a given resume-target activity is supported per workflow this wave (two
Delays in one workflow fail compilation). Node-scoped resume-target IDs (keyed byExecutableNodeId+ attribute ID) are the follow-up to lift this, and require a matching change in the resume resolver.
- Kind: Contributor (receives a signal and acts — push pattern).
- Signature:
ValueTask ReceiveSignalAsync(object signal, SignalContext context); - Usage: implement on activity classes to receive signals sent to the workflow.
ActivityBaseexposesReceiveSignalAsyncwhich dispatches to the activity'sISignalHandlerimplementation. - Not a fan-in aggregator — each activity implements this directly; there is no aggregating event handler. Signals are dispatched to activities in the workflow graph, not via the DI container.
- Sub-interface:
IBehavior : ISignalHandler— for behaviour objects composable onto activities.
Known implementations (shipped):
- Activity classes that override
ReceiveSignalAsyncin the codebase.
- Kind: Overridable single-impl (one handler expected at a time, injected by DI).
- Signature:
CompleteActivityAsync(IActivityExecutionContext context),CompleteActivityAsync(IActivityExecutionContext context, object result),CompleteActivityAsync(IActivityExecutionContext context, IEnumerable<string> outcomes),CompleteActivityAsync(IActivityExecutionContext context, IEnumerable<string> outcomes, object result). - Register:
services.Replace(ServiceDescriptor.Scoped<IActivityCompletionHandler, MyHandler>())— single-impl; a replacement steps aside the previous one. - Consumed by:
ActivityBase.CompleteAsync— resolvesIActivityCompletionHandlerfrom the execution context's service provider.
Known implementations (shipped):
Elsa.Workflows.Runtime.JavaScript—ActivityCompletionHandler(cross-domain — test implementation for JS-context activity completion)
- Kind: Strategy set (context-selected, exact-one owner per executable trigger node; not a contributor fan-in).
- Signature: additive stable nonblank
ProviderId; additiveCardinality(FanOutcompatibility default);ActivityTriggerStimulusResult Describe(ExecutableNode node). - Usage: at publish time the trigger extractor evaluates every registered strategy once for each compiler-marked node. Exactly one strategy must return
Recognized; zero owners and multiple owners fail rather than selecting by registration order. The selected provider's stable id appears in the non-persisted preflight outcome and contextual typed failures.Recognized([...])carries the node's stimulus identities(stimulusType, stimulusHash, correlationScope?, metadata).Recognized([])deliberately means an intentional non-start (for example a mid-flowHttpEndpointwithCanStartWorkflow = false) and succeeds without a binding. Providers read only the pinned publishedExecutableNode, never Design state or a running workflow. Public contract XML documentsWorkflowTriggerPreflightException; parser/calculator exceptions are wrapped at the publication boundary and retained as inner exceptions. - Register:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IActivityTriggerStimulusProvider, MyProvider>()).
Known implementations (shipped):
Elsa.Activities.Primitives—EventTriggerStimulusProvider(cross-domain — describes the named-eventEventstart trigger; stimulus typeEvent, hash over the event name).Elsa.Activities.Http—HttpEndpointTriggerStimulusProvider(cross-domain — describes theHttpEndpointstart trigger; stimulus typeHttpEndpoint, hash over the normalized request path so an inbound request routes to the matching published endpoint).Elsa.Activities.Scheduling—TimerTriggerStimulusProvider/CronTriggerStimulusProvider(cross-domain — describe theTimerandCronrecurring start triggers; stimulus typesTimer/Cron, hash over the interval / cron expression. These pair with the recurring-trigger schedule store + pump below: the stimulus identity is what the pump fires throughIStimulusRouter, and the same providers also implementIRecurringTriggerScheduleProviderto register the schedule at publish time).
The burst-coalescing persistence policy is an opt-in durability/throughput trade, enabled with
services.AddCoalescingRuntimeCheckpointPersistence() (in Elsa.Workflows.Runtime.Api.Coalescing). It is
not registered by default: the default runtime keeps ImmediateRuntimeCheckpointPersistencePolicy and
the contracts/decorators below are absent, so the default path is byte-identical. When enabled, it swaps the
policy to CoalescingRuntimeCheckpointPersistencePolicy and layers ambient-session decorators over the
checkpoint commit store, scheduler queue, post-commit outbox, and state stores. See
docs/runtime-durable-resumption.md
and the benchmark results.
Documented ADR 0033 deviation. The two coalescing interfaces below moved to the
Elsa.Workflows.Runtimeengine package instead of staying in.Corewith the other contracts: both expose the concreteRuntimeCoalescingSession(engine working state) on their signatures, so they cannot stand ahead of the engine, and their only consumers are the opt-in coalescing composition inRuntime.Apiplus its tests. They keep theirElsa.Workflows.Runtime.Core.Contractsnamespace. This canonical catalog covers both the contracts and engine-hosted deviations.
- Kind: Replacement (one ambient accessor exposes the active coalescing session to the decorators).
- Signature:
RuntimeCoalescingSession? Current { get; },IDisposable Push(RuntimeCoalescingSession? session). - Usage: an
AsyncLocalpush/pop stack that makes the current drain segment's in-memory working set ambient to the coalescing store/queue/outbox decorators, mirroring the existing ambient ownership-scope resolution. Only registered by the opt-in extension. - Default implementation:
AsyncLocalRuntimeCoalescingSessionAccessor(opt-in only).
- Kind: Replacement (one factory opens the per-drain coalescing scope and performs the quiescence flush).
- Signature:
IRuntimeCoalescingDrainScope Begin(string workflowExecutionId); scope exposesRuntimeCoalescingSession SessionandValueTask FlushAtQuiescenceAsync(CancellationToken). - Usage:
WorkflowDrainOrchestratoropens a scope around a drain when the factory is registered (greediest resolvable ctor), buffers intra-drain checkpoints in the session, and flushes one folded atomic commit at quiescence throughRuntimeCheckpointCommitter.CommitAsync(so W5 ownership fencing still gates it). Only registered by the opt-in extension. - Default implementation:
RuntimeCoalescingDrainScopeFactory(opt-in only).
- HTTP endpoint behaviour overrides:
Elsa.Workflows.Runtime.Http/EXTENSION_POINTS.md. - Repo-wide index:
EXTENSION_POINTS.md. - Constitutional basis: §2.6.1 + §2.6.2 + §2.22.1.