Ana içeriğe geç

Release v0.0.79

· 19 dakikalık okuma
vNext Team
Burgan Tech Engineering

Overview

This release turns functions into fully declared client contracts and closes long-standing gaps in transition authorization. Functions gain a declarative contractverbs, 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 typeStatus lockBusy checkBehavior
stateTransition / sharedTransitionholdsapplies409 while the instance is Busy
cancel / exitholdsexemptadmitted even on a Busy instance
updateDataexemptexemptadmitted 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, $self state change, and auto evaluation at order 90; a satisfied auto reserves ownership at the continuation boundary and advances the instance.
  • When the instance defines updateData and 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 attributeValuesPurpose
vnext.layerorchestration, executionIdentifies the vNext processing layer
vnext.span.categorybusiness, diagnosticSeparates 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 Business profile keeps service boundaries, transitions, task coordination, and task execution spans while suppressing cache operations, detailed task-execution phases, and ordered pipeline-step spans; Verbose restores 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 an Allow header 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 resolved sys-schemas contract; a violation returns 400 with field-level errors, reported identically to transition schema validation. outputSchema is declarative only — never enforced.
  • inputView / outputView — the sys-views contract 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
  • /info reports the allowed verbs, the invocation URL, and hasView / hasSchema flags 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 is 403. Built-in system functions (state, view, data, …) have no sys-functions component and return 404 from /info.
  • The workflow's function list moved behind a new built-in catalog function (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 gains modes, on both the local and remote resolution paths.

Reference: PR #858 — see also View component.

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 SubsAsync batches 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 ⇒ null or an empty list; a read failure or cap breach ⇒ RelatedInstanceAccessException — never a silent null.
  • IsCompleted (the target instance's status) and CorrelationCompleted (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-roles filtering): the engine is acting inside its own correlation frame. Mind that copying a related instance's x-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 availableTransitions from every state they are available in, gated on trigger type and availableIn, and their roles filter 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 / exit stay accepted on the request side). The entry's kind is cancel / updateData / exit.
  • A subflow's list merges the parent's updateData and exit, 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 responseShapeVersion into both the ETag material and cache key, so a client long-polling a parked instance sees the new entries instead of 304-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.roles is the global gate, the matching entry's roles narrows 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: availableTransitions and authorize both apply state + roles, and execution applies the state gate — well-known transitions can no longer be POSTed from a state excluded by availableIn (Transition:100024).
  • WorkflowValidator validates availableIn entries: 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-roles evaluates through the same core, so predefined and dynamic grants work in field-level visibility at runtime.
  • Six call sites that read ICurrentUser.Roles directly — treating a legacy role-header-only caller as role-less — now resolve caller roles consistently, including the custom-function 403 gate and the instance /data route.
  • 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.

  • activeCorrelations is unchanged — open correlations only; existing clients are unaffected.
  • A new correlations list carries the full set, active and completed, ordered by createdAt. Each entry adds isCompleted, 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 304 forever.
  • Under concurrent completion the active subset of correlations can be a moment fresher than activeCorrelations (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/v1 prefix — 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 BasePath structurally.
  • The orchestration host's previous config omitted the v1 segment 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 fields verbs, inputSchema, outputSchema, inputView, outputView including the rule-based slot forms (vnext-schema #128, #130), the view display object form { sdi, mdi } (#128), roles / availableIn on exitTransition and updateDataTransition (#129), and the role-scoped availableIn entry form { state, roles } (#132). Update @burgan-tech/vnext-schema in 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 + Allow on mismatch), inputSchema (400 on violation), declarative outputSchema, inputView/outputView — every slot single-reference or rule-based with fallback.
  • Function discovery: /info, /view, /schema routes at domain and instance scope, plus a role-filtered built-in catalog function linked from the state response.
  • View display accepts { sdi, mdi } per client mode; the legacy string stays first-class.
  • context.Related lets mapping scripts read the parent instance and sub-item correlations (ParentAsync / SubAsync / SubsAsync) without data duplication.
  • updateData / exit are discoverable in availableTransitions and their roles filter; availableIn entries 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; activeCorrelations unchanged.
  • UrlTemplates is a single BasePath (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, updateData is 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.category span taxonomy on Aether 1.0.34; Business detail level by default, Verbose restores the diagnostic waterfall.
  • Schema is 0.0.52.

vNext Runtime Platform Team Released August 13, 2026