Release v0.0.79
Overviewβ
This release turns functions into fully declared client contracts and closes long-standing gaps in transition authorization. Functions gain a declarative contract β verbs, inputSchema, outputSchema, inputView, outputView β enforced at invocation (405 with Allow for undeclared verbs, 400 with field-level errors on schema violations) (#858); every contract slot also accepts rule-based entries (first match wins, trailing rule-less fallback) and is discoverable through new /info, /view, /schema endpoints plus a built-in catalog function linked from the state response (#868, #869). Views declare per-mode display with the new { sdi, mdi } object form alongside the legacy string (#858). Mapping scripts can read related instances β the parent that started this instance, or its own sub items β through context.Related instead of duplicating data across the boundary (#857). updateData and exit are now discoverable in availableTransitions and their roles actually filter (#859), availableIn entries can be role-scoped per state (#870), and all role grant evaluation funnels through one evaluator, fixing surfaces that disagreed about the same caller (#860). The state function returns completed child correlations with terminal outcomes (#856), and UrlTemplates collapses to a single BasePath (#871). Fixes cover component version-resolution caching (#867) and subflow terminal-event deduplication (#855). Transition execution moves to Busy-as-mutex β the Busy status itself is the execution mutex, updateData becomes a status-neutral reserve transition admitted unconditionally (the one safe way to update data and advance an instance under parallel requests), cancel/exit bypass the busy check, and every InstanceData row is persisted the moment it is produced (#877). Workflow tracing gains a business-focused span taxonomy (vnext.layer, vnext.span.category) on Aether 1.0.34, defaulting to the Business detail level (#874). This release runs on component schema 0.0.52.
Featuresβ
Busy-as-mutex locking and the status-neutral updateData (#877)β
The chain-token locking model is replaced by Busy-as-mutex: the instance's Busy status is itself the execution mutex. Admission performs an ActiveβBusy check-and-set under a short status lock (5s lease); the pipeline and the auto-chain then run lock-free. The 330s-lease distributed lock is gone from the transition path entirely.
Each transition type participates differently:
| Transition type | Status lock | Busy check | Behavior |
|---|---|---|---|
stateTransition / sharedTransition | holds | applies | 409 while the instance is Busy |
cancel / exit | holds | exempt | admitted even on a Busy instance |
updateData | exempt | exempt | admitted unconditionally, status-neutral |
updateData is redesigned as a reserve transition β it never sets or settles Busy, so it can no longer strand an instance in Busy, and it is the one safe way to update data and advance an instance under parallel requests:
- On a plain instance it runs the normal transition pipeline β data write,
$selfstate change, and auto evaluation at order 90; a satisfied auto reserves ownership at the continuation boundary and advances the instance. - When the instance defines
updateDataand sits in an active subflow, the request is answered by the parent β never forwarded to or restarting the subflow: the parent's data is updated and left as-is. - Autos are evaluated after every updateData, so fan-in patterns ("accumulate, advance at the threshold") work under an updateData storm.
InstanceData writes are immediate: every row β task outputs included, parallel or sequential β persists the moment it is produced, with row identity (VersionNo = MAX(VersionNo)+1, version head + strategy, merged-content dedup) computed under a per-instance FOR UPDATE lock. The crash gap between tasks is closed: retries reuse the original transition record and the task journal skips completed tasks whose data is already on disk.
Authoring guidance that follows from the new model: give clients updateData (not stateTransition) in scenarios that push data while the instance is active; return delta-only output from mappings under parallel updateData (a full echo overwrites concurrent writers' fresher values); parallel branches at the same order need distinct task definitions. Parts of this change are breaking β see the v0.0.79 breaking changes announcement.
Reference: PR #877 β see also Workflow β Transition Execution Model.
Business-focused workflow tracing (#874)β
Workflow spans now carry a consistent business taxonomy, and the Aether packages move to 1.0.34 (which defaults the tracing detail level to Business):
| OpenTelemetry attribute | Values | Purpose |
|---|---|---|
vnext.layer | orchestration, execution | Identifies the vNext processing layer |
vnext.span.category | business, diagnostic | Separates the business waterfall from low-level diagnostic detail |
- Transition, task coordinator, and task execution spans carry workflow, instance, task, layer, and category metadata; incoming execution requests carry domain, flow, version, instance, task key/type attributes.
- The
Businessprofile keeps service boundaries, transitions, task coordination, and task execution spans while suppressing cache operations, detailed task-execution phases, and ordered pipeline-step spans;Verboserestores the full diagnostic waterfall. Suppressed pipeline steps can no longer rename the active transition span. - Configure globally via
appsettings.json:
{ "Telemetry": { "Tracing": { "DetailLevel": "Business" } } }
Reference: PR #874.
Function contract β verbs, input/output schemas and views (#858, #868)β
Functions previously declared no contract at all: any HTTP verb silently executed them and any body was accepted. The function attributes now carry a full declarative contract:
{
"attributes": {
"scope": "F",
"verbs": ["POST"],
"inputSchema": { "key": "calc-limit-input", "domain": "core", "flow": "sys-schemas", "version": "1.0.0" },
"outputSchema": { "key": "calc-limit-output", "domain": "core", "flow": "sys-schemas", "version": "1.0.0" },
"inputView": [
{ "rule": { "location": "./src/IsMobile.csx", "code": "<base64>" },
"view": { "key": "calc-limit-form-mobile", "domain": "core", "flow": "sys-views", "version": "1.0.0" } },
{ "view": { "key": "calc-limit-form", "domain": "core", "flow": "sys-views", "version": "1.0.0" } }
]
}
}
verbsβ the HTTP verbs the function accepts (GET,POST,PATCH,DELETE). A verb outside the declared set returns 405 with anAllowheader listing the declared verbs. Absent or empty means every routed verb is accepted (pre-existing behavior).inputSchemaβ when set and the request carries a body, the body is validated against the resolvedsys-schemascontract; a violation returns 400 with field-level errors, reported identically to transition schema validation.outputSchemais declarative only β never enforced.inputView/outputViewβ thesys-viewscontract a client renders to collect the function's input or present its output.- Every slot accepts a single reference or rule-based entries: entries are evaluated in declaration order, the first matching rule wins, and a trailing rule-less entry is the fallback β the same concept state and transition views already use. When nothing matches, the slot resolves to "no contract" (validation skips, content routes return 404) rather than an error.
- Both gates are opt-in β existing functions declaring neither verbs nor schemas behave exactly as before.
Reference: PRs #858, #868 β see also Custom Functions and Function component.
Function discovery β /info, /view, /schema endpoints and the catalog function (#868, #869)β
Six new GET routes answer may I run this function, with which verb, at which URL, and which view/schema applies right now:
GET {domain}/functions/{fn}/info
GET {domain}/functions/{fn}/view?target=input|output
GET {domain}/functions/{fn}/schema?target=input|output
GET {domain}/workflows/{wf}/instances/{id}/functions/{fn}/info
GET {domain}/workflows/{wf}/instances/{id}/functions/{fn}/view?target=input|output
GET {domain}/workflows/{wf}/instances/{id}/functions/{fn}/schema?target=input|output
/inforeports the allowed verbs, the invocation URL, andhasView/hasSchemaflags saying whether following the href returns content now β a rule may match on a later call, so the href is emitted regardless.- Discovery enforces the same scope and role gates as execution (shared
IFunctionAccessPolicy), so a caller who cannot invoke a function cannot learn its shape either β denial is403. Built-in system functions (state,view,data, β¦) have nosys-functionscomponent and return404from/info. - The workflow's function list moved behind a new built-in
catalogfunction (GET .../instances/{id}/functions/catalog), returning{ "functions": [ { "name", "version", "scope", "href" } ] }in declaration order. The catalog is role-filtered β a function the caller could not invoke is not advertised β and each href follows the function's scope (Dβ domain route,F/Iβ instance route). - The state response now carries only a pointer:
"functions": { "hasFunctions": true, "href": ".../functions/catalog" }β the poll hot path reads no function components at all.
Reference: PRs #868, #869 β see also Built-in Functions β Catalog and REST API.
View SDI/MDI display modes (#858)β
attributes.display on a view now accepts an object declaring the display per client mode, alongside the legacy bare string:
{ "display": { "sdi": "full-page", "mdi": "popup" } }
- SDI (single-document interface) is what the legacy string always described; the string form keeps its exact meaning and round-trips unchanged.
- MDI (multi-document interface) clients β several documents open side by side β can now be told how the view lands. Both modes share the same value set:
full-page,popup,bottom-sheet,top-sheet,drawer,inline. - The view response keeps
display(the SDI value) and gainsmodes, on both the local and remote resolution paths.
Reference: PR #858 β see also View component.
Related instance access from mapping scripts β context.Related (#857)β
Mapping scripts can now read a related workflow instance's data β the parent that started this instance as a SubFlow/SubProcess (one hop up), or one of this instance's own correlations (one hop down) β instead of duplicating data across the boundary with output mappings:
// input binding on a subflow β read the parent
var parent = await context.Related.ParentAsync();
var limit = GetPropertyValue<decimal>(parent?.Data, "creditLimit", 0m);
// view condition on a parent β is the KYC child done?
var kyc = await context.Related.SubAsync("kyc-flow");
return kyc?.IsCompleted == true;
// aggregate over repeated subprocesses
var uploads = await context.Related.SubsAsync("doc-upload");
return uploads.Count(u => u.CorrelationCompleted == true) >= 3;
- Lazy and memoized: nothing is pre-fetched, results are cached for the script context's lifetime, and
SubsAsyncbatches into a single query. Resolutions are capped per context (Workflow:Scripting:RelatedAccess:MaxResolutionsPerContext, default 10). - Absence is data; failure is a fault. No parent / no correlation β
nullor an empty list; a read failure or cap breach βRelatedInstanceAccessExceptionβ never a silentnull. IsCompleted(the target instance's status) andCorrelationCompleted(whether the relationship is closed) are separate fields on purpose β they disagree during the completion window.- Reads are unfiltered by design (no query-role check, no
x-rolesfiltering): the engine is acting inside its own correlation frame. Mind that copying a related instance'sx-roles-restricted field into this instance's data makes it readable by anyone entitled to read this instance. - Cross-domain reads route over internal endpoints with the usual timeout/retry/circuit-breaker stack; same-domain reads never leave the process.
Reference: PR #857 β see also Mappings β Related.
updateData and exit discovery + role filtering (#859)β
cancel, updateData and exit all carried a roles collection, but only cancel was ever listed in availableTransitions β so roles on the other two was dead configuration and clients had to hard-code well-known keys.
- All three well-known transitions now appear in
availableTransitionsfrom every state they are available in, gated on trigger type andavailableIn, and theirrolesfilter the list through the same three filter surfaces as any other transition. - The configured key is listed, never the well-known alias (
update-parent-data/exitstay accepted on the request side). The entry'skindiscancel/updateData/exit. - A subflow's list merges the parent's
updateDataandexit, so the Client Workflow Manager drives them through the same loop it uses for state and shared transitions. - The state response cache now folds a
responseShapeVersioninto both the ETag material and cache key, so a client long-polling a parked instance sees the new entries instead of304-ing forever.
Reference: PR #859 β see also Workflow component and Built-in Functions β State.
Role-scoped availableIn (#870)β
availableIn could restrict where a shared or well-known transition is offered, but not who may use it there. Each entry now accepts either a bare state key or { state, roles }, mixable in one array:
"availableIn": [
"review",
{ "state": "approval", "roles": [ { "role": "backoffice.supervisor", "grant": "allow" } ] }
]
- Composition is AND:
transition.rolesis the global gate, the matching entry'srolesnarrows it for that state. Both levels evaluate through the same evaluator, so DENY-wins and allowlist/blacklist semantics are identical at both levels. A role-less entry behaves exactly like the legacy bare string. - The three surfaces answering "may this caller run this transition?" now agree:
availableTransitionsandauthorizeboth apply state + roles, and execution applies the state gate β well-known transitions can no longer be POSTed from a state excluded byavailableIn(Transition:100024). WorkflowValidatorvalidatesavailableInentries: the state must exist, duplicates are rejected, and per-entry role grants get the same dynamic-role syntax check as everywhere else.
Reference: PR #870, schema PR vnext-schema #132 β see also Workflow component and Authorization.
Unified role grant evaluator (#860)β
Function.roles, Transition.roles, state/workflow queryRoles and schema x-roles all evaluate the same thing β a grant set against the caller's roles β but the rule was implemented four times and had drifted. All grant evaluation now funnels through one batch-scoped RoleGrantEvaluator:
- Behalf-of grants (
$InstanceBehalfOfStarter,$PreviousBehalfOfUser) in the human-task list were compared against the wrong identity field; a role-less caller was denied before blacklist rules were reached. Both fixed β the human-task list now agrees with transition execution. x-rolesevaluates through the same core, so predefined and dynamic grants work in field-level visibility at runtime.- Six call sites that read
ICurrentUser.Rolesdirectly β treating a legacyrole-header-only caller as role-less β now resolve caller roles consistently, including the custom-function 403 gate and the instance/dataroute. - Batching: N grant sets cost one round of I/O instead of N.
Some of these corrections change observable behavior β see the v0.0.79 breaking changes announcement for the migration notes.
Reference: PR #860 β see also Authorization.
State response β completed child correlations (#856)β
The state (long-poll) function only exposed open child correlations; which sub items ran and how each ended was absent.
activeCorrelationsis unchanged β open correlations only; existing clients are unaffected.- A new
correlationslist carries the full set, active and completed, ordered bycreatedAt. Each entry addsisCompleted,completedAt,terminalOutcome(completed/faulted/canceled),currentState,stateChangedAt,createdAt. - The state ETag now moves on every correlation mutation β a sub item starting, terminating, reverting or advancing its own state β so long-polling clients observe the updated list instead of receiving
304forever. - Under concurrent completion the active subset of
correlationscan be a moment fresher thanactiveCorrelations(documented trade-off).
Reference: PR #856 β see also Built-in Functions β State.
UrlTemplates β a single BasePath (#871)β
Nineteen per-endpoint URL templates per host collapse into one setting:
"UrlTemplates": { "BasePath": "/api/v1/monitor" }
- Omit the section entirely and the application serves its own
/api/v1prefix β the orchestration host now declares nothing. - Resolution rule:
Effective(X) = override(X) ?? Normalize(BasePath) + BuiltInRelative(X). A per-endpoint override is a complete path used verbatim β any value authored in the previous all-templates style, including env-var overrides, keeps working unchanged. - A template added to the runtime but forgotten in config can no longer silently fall back to a prefix-less path β it inherits
BasePathstructurally. - The orchestration host's previous config omitted the
v1segment its routes actually require; its hrefs now point at/api/v1/β¦. If your gateway genuinely serves/api/{domain}/β¦, set"BasePath": "/api"β see the breaking changes announcement.
Reference: PR #871 β see also URL Templates.
Behavior Changesβ
This release aligns authorization surfaces and fixes silently-broken validation, which changes observable behavior in a few places: x-roles DENY semantics, deny-only sets for role-less callers, the human-task list, dynamic role grant validation at deploy time, the availableIn execution gate for well-known transitions, and orchestration host href prefixes. The locking rework (#877) is also behavior-changing: state/shared transitions now answer 409 while the instance is Busy (instead of queueing on a chain lock), updateData no longer forwards to subflows and never touches Busy, and the chain-token columns are dropped by migration. Each item, its impact direction, and the migration steps are documented in the dedicated v0.0.79 breaking changes announcement.
Fixesβ
- Component version resolution scoped to a generation token β publishing a component left range-resolution cache keys (
version: "1","1.2",latest) frozen at the previously resolved version; a workflow referencing"1"kept rendering the old view indefinitely. Version-request answers are now keyed under a per-component generation token that every publish/invalidate refreshes, covering non-monotonic publishes, deactivations, and alias spellings. Pinned full-version reads are unaffected (#867). - Subflow terminal event dedup made cheap and settlement-aware β duplicate terminal sub-item deliveries (
instance.sub.completed/.faulted/.canceled) colliding with the in-flight original lost the per-subInstance lock, surfaced a transient 503, and waited a full broker re-delivery cycle (~63s observed). A read-only pre-lock settlement probe plus a bounded lock wait (WorkflowExecutionOptions.SubItemTerminalLockRetry) absorb the duplicate without error noise (#855).
Configuration Updatesβ
Configuration for v0.0.79:
{
"runtimeVersion": "0.0.79",
"schemaVersion": "0.0.52"
}
Note: Schema version advances to 0.0.52 (from
0.0.51). The bump adds the function contract fieldsverbs,inputSchema,outputSchema,inputView,outputViewincluding the rule-based slot forms (vnext-schema #128, #130), the viewdisplayobject form{ sdi, mdi }(#128),roles/availableInonexitTransitionandupdateDataTransition(#129), and the role-scopedavailableInentry form{ state, roles }(#132). Update@burgan-tech/vnext-schemain your domain project before validating against this runtime.
Container images: published at tag 0.0.79 under ghcr.io/burgan-tech/vnext/*, Cosign-signed (keyless OIDC) with SBOM + provenance. Immutable digests are listed in the GitHub release.
Issues Referencedβ
- vnext #858 β View SDI/MDI display modes and function verb/schema/view contract.
- vnext #868 β Rule-based view/schema contracts, an info endpoint, and state-response discovery.
- vnext #869 β Move the state response function list behind a catalog function.
- vnext #857 β Read related instance data from mapping scripts.
- vnext #859 β List updateData/exit in availableTransitions and enforce their roles.
- vnext #870 β Role-scope availableIn per state and align the three auth surfaces.
- vnext #860 β Funnel all role grant evaluation through one evaluator.
- vnext #856 β State function returns completed child correlations.
- vnext #871 β Collapse per-endpoint URL templates into a single BasePath.
- vnext #867 β Scope component version resolutions to a generation token.
- vnext #855 β Make subflow terminal delivery dedup cheap and settlement-aware.
- vnext #877 β Busy-as-mutex locking, status-neutral updateData, and immediate InstanceData persistence.
- vnext #874 β Add business-focused workflow tracing with Aether 1.0.34.
Summaryβ
- Function contract:
verbs(405 +Allowon mismatch),inputSchema(400 on violation), declarativeoutputSchema,inputView/outputViewβ every slot single-reference or rule-based with fallback. - Function discovery:
/info,/view,/schemaroutes at domain and instance scope, plus a role-filtered built-incatalogfunction linked from the state response. - View
displayaccepts{ sdi, mdi }per client mode; the legacy string stays first-class. context.Relatedlets mapping scripts read the parent instance and sub-item correlations (ParentAsync/SubAsync/SubsAsync) without data duplication.updateData/exitare discoverable inavailableTransitionsand theirrolesfilter;availableInentries can carry per-state role grants (AND with transition roles).- One role grant evaluator behind transitions, functions, queryRoles and
x-rolesβ surfaces no longer disagree; see the breaking changes announcement. - State response exposes completed child correlations with terminal outcomes;
activeCorrelationsunchanged. UrlTemplatesis a singleBasePath(default/api/v1); per-endpoint overrides stay verbatim-compatible.- Fixes: generation-token component cache invalidation, subflow terminal dedup.
- Busy-as-mutex: the Busy status is the execution mutex β state/shared transitions 409 on Busy, cancel/exit bypass the busy check,
updateDatais an unconditional, status-neutral reserve transition (answered by the parent even in an active subflow); InstanceData rows persist immediately with per-instance locked versioning. - Tracing:
vnext.layer/vnext.span.categoryspan taxonomy on Aether 1.0.34;Businessdetail level by default,Verboserestores the diagnostic waterfall. - Schema is 0.0.52.
vNext Runtime Platform Team Released August 13, 2026
