elsa-workflows / elsa-workflows/elsa-core
PublishEvent / BackgroundStimulusDispatcher bypasses transactional workflow-dispatch outbox
- Dominant language
- C#
- Stars
- 7.9k
- Forks
- 1.5k
- Avg merge
- 15h 22m
- Merged PRs (30d)
- 114
Description
## Problem
When `WorkflowDispatcherOptions.UseTransactionalOutbox` is enabled, **child workflow dispatches** made during an execution cycle are durable (written to the outbox and delivered after parent state commits). **Published events from the same cycle are not** — `PublishEvent` always goes through `IStimulusDispatcher` / `BackgroundStimulusDispatcher`, which enqueues a mediator background command immediately and never touches `TransactionalWorkflowDispatcher` / `IWorkflowDispatchOutbox`.
That is a silent durability gap: hosts that turn on the outbox for crash-safety still lose in-workflow events if the process dies between activity complete and background command processing.
## Evidence (main)
### PublishEvent always picks the thin stimulus path
`src/modules/Elsa.Workflows.Runtime/Activities/PublishEvent.cs`:
```csharp
await publisher.PublishAsync(eventName, correlationId, workflowInstanceId, null, payload, true, context.CancellationToken);
// ^^^^ asynchronous: true
```
`EventPublisher` with `asynchronous: true` calls `IStimulusDispatcher.SendAsync`, not `IWorkflowDispatcher`.
### BackgroundStimulusDispatcher — no outbox, drop caller CT
`BackgroundStimulusDispatcher.SendAsync` → `commandSender.SendAsync(DispatchStimulusCommand, CommandStrategy.Background, …, CancellationToken.None)` and returns `DispatchStimulusResponse.Empty`.
Default DI registers only that implementation for `IStimulusDispatcher` (no transactional decorator).
### Contrast — Trigger/Resume on IWorkflowDispatcher *do* get outbox
`TransactionalWorkflowDispatcher.DispatchAsync(DispatchTriggerWorkflowsRequest|DispatchResumeWorkflowsRequest)` enqueues `WorkflowDispatchOutboxItemKind.TriggerWorkflows` / `ResumeWorkflows` when inside a `WorkflowExecutionContext` and `UseTransactionalOutbox` is true — then the same eventual `IStimulusSender.SendAsync` runs after commit.
So the **durable** async stimulus path already exists on `IWorkflowDispatcher`; `PublishEvent` simply does not use it.
### Same destination, different reliability
`DispatchStimulusCommandHandler` and `DispatchWorkflowCommandHandler` (Trigger/Resume) both end at `IStimulusSender.SendAsync`. Only the entry façade differs — and with it, crash semantics.
## Expected
- In-workflow async stimulus (at least `PublishEvent`) should honor the same transactional-outbox contract as in-workflow workflow dispatch when the option is enabled — **or** docs/options must state that outbox covers workflow dispatch only, not events.
- Prefer subtractive fix: route `PublishEvent` / async `EventPublisher` through the durable façade (see #8149), rather than building a second outbox for `IStimulusDispatcher`.
## Suggested follow-through
1. Decide: events are in scope for `UseTransactionalOutbox` (recommended for consistency) or explicitly out of scope (document + warn on `PublishEvent`).
2. If in scope: change `EventPublisher` async path to `IWorkflowDispatcher.DispatchAsync(new DispatchTriggerWorkflowsRequest(...))` (or a single collapsed async stimulus API from #8149) so `TransactionalWorkflowDispatcher` applies.
3. Conformance/integration test: with outbox on, `PublishEvent` inside a parent workflow must not lose the stimulus across a simulated crash between parent commit and delivery (mirror existing workflow-dispatch outbox tests).
## Not a duplicate of
- #8149 — dual/triple stimulus entry surface (root API smell; this issue is the concrete durability bug)
- #8136 — obsolete inbox DI
- #8135 — unmatched-resume enqueue field drops
- #8042 — activation strategies on dispatched definitions
- Wiki outbox notes for `TransactionalWorkflowDispatcher` — describe workflow dispatch only today
## Milestone
Unset — Elsa 3 Issue Triage / Crew Lead place. Soft proposal: Ready-after-prune or Backlog with prio once confirmed against production outbox users; not auto 3.9. Do not auto-assign Engineer.
Contributor guide
Research direction
Start with src/modules/Elsa.Workflows.Runtime/Activities/PublishEvent.cs, then trace EventPublisher, BackgroundStimulusDispatcher, and TransactionalWorkflowDispatcher. Compare the existing workflow-dispatch outbox tests and confirm how DispatchStimulusCommand reaches IStimulusSender. Done when the outbox scope is decided and either crash-safe PublishEvent coverage exists or the limitation is documented and warned about.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100