Compare: 3.8.4...3.9.0
Elsa 3.9.0 — Release Notes
Minor release, shipped as an aligned 3.9.0 train with Elsa Studio 3.9.0 and Elsa Extensions 3.9.0. Upgrade all three together.
The headline is a new access and dashboard model. Most of the fixes close tenant-isolation gaps, so if you run Elsa with multitenancy, read the upgrade notes below before rolling out.
🌟 Highlights
- Permission-based access everywhere, including the dashboard. The dashboard is now open to every signed-in user. Each section, card and widget shows only what that user's permissions allow.
dashboard:viewstill reads everything, so existing roles keep working. (#8562, #8564, #8589) - Real sign-out for Elsa Identity. The new
POST /identity/logoutendpoint revokes the whole sign-in session. Copied refresh tokens stop working after sign-out. (#8552) - Multitenant bookmark queue works. Queued bookmarks now resume in every tenant, and nothing loads the full tenant list to do it. Thanks to @tulrichtrimble for the diagnosis in #8183. (#8382)
- Tenant isolation hardening. The default in-memory key-value store, bulk saves and imports, and the administrative pause no longer cross tenant boundaries. (#8431, #8490, #8455)
🔐 Access and dashboard model
-
Dashboard per-section permissions (#8562). Each part is readable with
dashboard:viewor the permission of its data:- workflow-instance metrics, trends, recent activity, needs-attention and hotspots:
workflows/instances:view - runtime status:
workflows/runtime:view - structured and console logs:
diagnostics/structured-logs:viewanddiagnostics/console-logs:view
A section the caller can't read comes back as
Capability = Unauthorized, with no data. Dashboard contributions from third-party modules that don't declare a permission still needdashboard:view. - workflow-instance metrics, trends, recent activity, needs-attention and hotspots:
-
"Any of" endpoint permissions (#8564). There is a new
RequireAnyPermission(...)for endpoints that accept either of several permissions. It is recorded inEndpointPermissionRegistrythrough the newFindRequirement/AllRequirements. -
No wasted dashboard queries (#8589). A caller without
workflows/instances:viewno longer triggers instance-store queries on the overview. Contributors can checkDashboardContext.CanRead, wherenullmeans unrestricted. -
Installed features readable by any signed-in user (#8549).
GET /features/installedis authenticated-only, so Studio can render the right menus.system/features:viewstays valid but no longer gates anything. -
Definition viewers can open the designer (#8556). Workflows that can be used as activities are now visible to every signed-in user in the tenant. Listing versions now needs
workflows/definitions:view. -
Users with no grants get a
permissionsclaim (#8566). Elsa-issued tokens always carrypermissions. It is the sentinel"none"when the user holds no grants, so Studio can tell "no permissions" apart from "unknown". Third-party OIDC tokens without the claim are unchanged. -
Refresh tokens are bound to the user id (
sub) (#8574). A deleted user's refresh token can no longer be used by a new user with the same name. A missing, blank or conflicting subject now gets 401.
🐛 Fixes
- Bookmark queue resumes in every tenant (#8382, via #8385 and #8413). Each tenant's worker keeps its own tenant scope, signals are routed per tenant, and the in-memory queue only reads the current tenant's items. Supersedes #8183, with credit to @tulrichtrimble. The wider "loads every tenant" scaling work is tracked in #7173.
- Named
WithVariable(name, value)values survive suspend/resume (#8159, via #8166). See the behaviour change below. - Studio/API edits win over a stale imported
OriginalSource(#8305, via #8418, a backport of #8331). Editing a file-imported definition in Studio or through the API now changes what runs and what is exported. - Shutdown drain no longer promotes Finished or Cancelled instances to Interrupted (#8419, via #8421). A workflow cancelled at the drain deadline stays Cancelled. The
WorkflowInterruptedforensic log is still written.TryMarkInterruptedAsyncrefuses every Finished row, and the in-memory store now applies tenant visibility. - In-memory key-value store is tenant-isolated again (#8431, via #8434). This restores the 3.8.1 fix that hadn't been carried forward. The workflow-dispatch outbox's legacy scan marker is now per tenant.
- Administrative pause is tenant-agnostic (#8455, via #8486). A resume from another tenant no longer leaves a stale pause behind.
- Legacy pause adoption can't bring back a cleared pause (#8529, via #8539). It is adopted with a count-checked delete, so only one node writes the new key.
- EF Core bulk save refuses cross-tenant overwrites (#8490, via #8491 and #8507).
SaveManyAsyncrefuses a batch that would overwrite another tenant's row, including key variants under case-insensitive or trailing-space collations. Workflow-instance import stamps the importing tenant instead of trusting the file.
⚠️ Upgrade notes and breaking changes
Upgrade with Studio 3.9.0 and Extensions 3.9.0. Studio 3.9 requires core 3.9. Extensions 3.9.0 is built against core 3.9.0.
Behaviour changes
- Named
WithVariable(name, value)now uses workflow storage (#8166). It used to be memory-only, which meant the value was lost after a bookmark. If you relied on the old behaviour, pass an explicit storage driver or use aVariable<T>you configure yourself. - ElsaScript and other source-based workflows are skipped by the reference updater (#8418). When a referenced workflow is published, these consumers are left alone and a warning is logged, naming the consumer's definition id and the referenced workflow. They keep pointing at the old version until you update them by hand. JSON-based consumers are updated as before.
- Refresh with a missing, blank, conflicting or unknown subject now returns 401 (#8573). It used to return 200 with
isAuthenticated: false. Treat 401 as "sign in again". PauseAsync/ResumeAsyncwith an already-cancelled token now throw (#8486). They used to complete.MemoryKeyValueStore.SaveAsyncthrowsInvalidOperationExceptionon a cross-tenant key conflict (#8434).
Sign in again (refresh tokens without sub)
Refresh tokens that don't carry sub are rejected (#8573). Only 3.8.0-preview1 issued refresh tokens like that; tokens from 3.0–3.7 were already rejected. Users holding one need to sign in again.
Database migration (EF Core)
- New
RevokedSessionstable for SQL Server, SQLite, PostgreSQL, MySQL and Oracle (#8552). Apply it before upgraded hosts refresh tokens. Elsa's startup auto-migration covers it unlessRunMigrationsis off.
Multitenant rolling upgrades
- Bookmark queue lock rename (#8385). Named tenants now use the distributed lock
DistributedBookmarkQueueWorker:{tenantId}. The default tenant keepsDistributedBookmarkQueueWorker, so single-tenant installs see no change. While old and new nodes run side by side, both can process the same queued item. Processing an item after its bookmark has been consumed does nothing. In the rare case that both nodes resume before the first one commits, the race is the same as two in-process resumers hitting one bookmark. - Administrative pause key (#8486). The persisted pause key is now
elsa.quiescence.host-pause.{shell}(waselsa.quiescence.pause.{shell}). On startup, a 3.8 default-tenant ('') orNULLrow, or a named-tenant row visible under the activating tenant, is moved to the new*key and the old row is deleted.- Mixed-version rollout: the first 3.9 node deletes the old key, so a 3.8 node that restarts later won't see the pause. Finish the upgrade before relying on
AcrossReactivationsduring a rolling mixed-version deploy. - Optional cleanup of leftovers (the old key is never
*; the table name follows your configured runtime key-value table):DELETE FROM ElsaKeyValuePairs WHERE Id LIKE 'elsa.quiescence.pause.%';
- Residual: a node that resumes while another is still finishing an earlier adoption can recreate the new key. Sequential restarts are idempotent.
- Mixed-version rollout: the first 3.9 node deletes the old key, so a 3.8 node that restarts later won't see the pause. Finish the upgrade before relying on
- Outbox legacy scan marker (#8434). It is now
Elsa:WorkflowDispatchOutbox:State:LegacyScanCompleted:{tenantId}. The old shared key isn't read, so each tenant runs one extra legacy scan after the upgrade.
API and extensibility changes
IKeyValueStorehas a newTryDeleteAsyncmember with a default implementation, so it is source- and binary-compatible. Shared stores should override it with a single count-checked delete, and decorators must forward it. Extensions 3.9.0 does this for Dapper and MongoDB. (#8539)MemoryKeyValueStoreandKeyValueWorkflowDispatchOutboxStoretake a new optionalITenantAccessorconstructor parameter. (#8434)DefaultIdentityRefreshTokenService's public constructor now requires aSessionRevoker. Resolve it from DI. (#8552)DashboardRuntimeStatusandDashboardWorkflowInstanceMetricshave a newCapabilityfield, which defaults toAvailable.DashboardQueryandDashboardContexthave a new optionalCanRead.DashboardContextstays binary-compatible with 3.8. (#8562, #8589)EndpointPermissionRegistry.Find/Allleave out endpoints that accept any of several permissions. UseFindRequirement/AllRequirementsfor those. (#8564)TryMarkInterruptedAsynckeeps itsallowFinishedCancelledparameter for binary compatibility, but it is now ignored. (#8421)
🚧 Known limitations
- Drain force-cancel doesn't reach running activities through their cancellation token. An interrupted instance can end up Finished or Cancelled instead of resumable. A fix is planned for 3.10. (#8489)
- Sessions: password reset and user deletion don't end existing sessions, and there's no "sign out everywhere" yet.
- Per-node revocation with MongoDB or Dapper: session revocation is held in memory on each node. Signing out on one node doesn't revoke the session on the others, and a restart clears it. EF Core stores revocations in
RevokedSessions. - Access tokens stay valid until they expire after sign-out. They are short-lived by design (up to 15 minutes).
- Permission changes apply when the token is refreshed. The
PermissionStampsetting has no effect in 3.9. (#8575) - The UI updates only after a page reload when a user's permissions change or the user is disabled.
- Other tabs and Blazor Server circuits stay signed in after sign-out until their next call, because revocation is checked on the server.
🙏 Credits
- @tulrichtrimble for diagnosing the multitenant bookmark queue bug (#8183).
- Everyone who reported issues and tested the 3.9 previews.