Skip to main content

Release v0.0.81–84

· 15 min read
vNext Team
Burgan Tech Engineering

Overview

This post consolidates four interim versions — v0.0.81, v0.0.82, v0.0.83 and v0.0.84 — shipped in quick succession between August 17 and August 20. The headline change is in v0.0.84: instance query handling moves from fail-open to fail-closed, so a malformed or unsupported filter, sort, groupBy or aggregation is rejected with HTTP 400 before execution instead of silently widening the query and returning wrong rows (#881); the same release gives scheduledTransitions entries the uniform link metadata every other transition entry carries, at response shape v7 (#894). The interim versions are corrective: v0.0.81 closes the three missing links that detached an Elastic APM trace subtree (#887), makes script compilation race-free under load (#888) and repairs the release pipeline so a partially-published release can be completed rather than abandoned (#886); v0.0.82 puts authoritative Busy reads and writes in isolated transactions (#890); and v0.0.83 lets error-boundary transitions re-enter a Busy instance (#892). This release runs on component schema 0.0.52.


Features

Fail-closed instance query validation (#881)

Shipped in v0.0.84 (tracked under milestone v0.0.83).

Instance queries were fail-open: an unsupported operator, an unknown field, a malformed sort or an unparseable aggregation was silently dropped, and the query ran without it. The caller got HTTP 200 and a result set that was quietly wider — or differently ordered — than the one they asked for. On a query used to drive a work list, that is a correctness bug the client cannot detect.

A new shared InstanceQueryValidator now validates filter, sort, groupBy and aggregation parameters before execution, on every instance-query surface (orchestration and monitoring alike):

Rejected inputResult
Malformed or unsupported filter400, Validation:900011
Invalid sort / groupBy / aggregation JSON, unknown field, unsafe attribute path400, Validation:900012
Legacy filter combined with an aggregation400
Empty or malformed aggregation request400

The rejection carries operator suggestions and consolidates every problem in the request into one response, rather than failing on the first.

The change reaches further than the HTTP surface:

  • GetInstancesTask fails instead of running unfiltered. A workflow task whose query parameters are invalid now fails the task and surfaces the definition defect, rather than executing a query that matches everything.
  • The -field sort shorthand is removed. "-CreatedAt" in a GetInstancesTask definition is no longer supported; use the JSON orderBy syntax.
  • Compilation paths throw rather than degrade. FilterCompilationException and SchemaFilterValidationException are raised when a query cannot be translated safely, aligning boundary validation with what the SQL builders actually support. InstanceOrderByApplicator fails fast on an unknown instance column instead of discarding the sort key.
  • Legacy and JSON filter/spec implementations no longer fall back to "return all rows" when a filter is invalid or matches no mapped property.

New log events cover rejected query parameters, filter-compilation drift and invalid task-authored filters, so a definition defect is visible in operations rather than only at the caller.

Reference: PR #881 — see also Instance filtering and REST API.

Shipped in v0.0.84.

v0.0.80 introduced scheduledTransitions with a name, kind and execution time. A client rendering the transition list then had to special-case those entries, because every other entry carries transition, view and schema links. Scheduled entries now carry the same link metadata:

{
"name": "escalate-to-supervisor",
"kind": "scheduled",
"executeAtUtc": "2026-08-20T14:32:05Z",
"href": "/api/v1/{domain}/workflows/{flow}/instances/{id}/transitions/escalate-to-supervisor",
"view": { "href": "...", "hasView": false, "loadData": false },
"schema": { "href": "...", "hasSchema": false }
}
  • The shape is uniform with the rest of the list, so a client learns one entry shape.
  • All link capabilities are reported as unavailable (hasView: false, hasSchema: false, loadData: false) and the href is not an invitation to call it — scheduled transitions stay System-actor gated. Advertising the href regardless keeps the shape uniform without granting anything.
  • The state-function response shape version advances to v7, which invalidates cached responses built on the v6 payload.

Reference: PR #894 — see also Built-in Functions → State.

Elastic APM trace tree repair (#887)

Shipped in v0.0.81.

A client's transition finally renders as one unbroken tree in Kibana's APM waterfall, from the inbound PATCH …/transitions/{key} down to the outbound task request. Three separate links between the entry point and the remote call were never exported, and each produced the same shape: a span whose parent id went out on the wire but whose parent document no backend ever received. Elastic APM re-parents such a span to the trace root, so the entire Execution subtree — sidecar hops, the Execution transaction, and the remote HTTP call inside it — detached.

beforeafter
Orphans on the business pathExecution subtree detached0
Orphaned gRPC client spans60
Orphaned state-store / lock spans55 (invisible — the sidecar exported nothing)0

The three fixes:

  1. Dapr sidecars exported nothing. The tracing block in dapr/config.yaml was authored under otlp:, a key Dapr's TracingSpec does not have, so it was silently ignored — the sampler still initialized and the sidecar still created and propagated span ids while building no exporter, which is why there was no error to find. The field is otel, and both protocol and isSecure turn out to be required rather than optional: Dapr builds no exporter without an explicit protocol, and isSecure defaults to TLS, which a plaintext collector refuses.

    spec:
    tracing:
    samplingRate: "1"
    otel:
    endpointAddress: "otel-collector:4317"
    protocol: "grpc"
    isSecure: false

    isSecure: false is right for a plaintext local collector, not universally — a TLS collector needs that line changed.

  2. No gRPC client instrumentation. Aether registers AspNetCore and HttpClient instrumentation only, but Grpc.Net.Client — which every Dapr.Client call goes through — creates its own activity, and the System.Net.Http span nests under it. The discriminator was exact: every HTTP/2 client span in a trace was orphaned and every HTTP/1.1 one correctly parented. Registering OpenTelemetry.Instrumentation.GrpcNetClient exports the parent; one registration covers all five hosts.

  3. Duplicate sidecar spans filtered. Turning on sidecar export revealed 55 pre-existing holes, all state-store or lock calls. The collector now drops the sidecar's duplicate — it carries only the sidecar's own internal handling time, the app already reports the call with its timing, and it cost ~50 detached spans per transition. The filter is scoped by instrumentation scope (dapr-diagnostics), deliberately not by span name, because the app-side client span carries the same name suffix. CallLocal/* is untouched — those are the spans that reconnect Orchestration to Execution.

Telemetry:Tracing:DetailLevel stays Business throughout: that is the level the environments run, so the tree has to be correct at that level rather than only at Verbose.

Reference: PR #887 — see also Observability.

Script compilation race fix (#888)

Shipped in v0.0.81.

Under load, subflow completions failed with:

Instance:100030 — SubFlow output mapping failed for parent instance '<id>':
Could not load file or assembly 'Script_6A6A92A6A310A0D7, …'.
Assembly with same name is already loaded (System.IO.FileLoadException)

Three conditions had to intersect, and none is sufficient alone. Compilation was check-then-actTryGetValue → miss → Roslyn emit (~100–500 ms) → LoadFromStreamTryAdd, with the vulnerable window covering the whole emit; the assembly's simple name derives from the cache key, so two concurrent compilations of the same script produced the same assembly name. A declared scripts.helpers set makes the load context shared and singleton-lifetime, and an AssemblyLoadContext cannot hold two assemblies with the same simple name — which is why this only ever appeared on helper-declaring flows. And parallel completions of distinct instances compile the same mapping under the same cache key. The consequential damage was worse than the noise: a failed output mapping is treated as permanent, so every loser faulted an otherwise healthy instance.

CSharpEvaluator now guarantees:

  • One compile per cache keyGetOrAdd with Lazy<T> at ExecutionAndPublication; waiters block and receive the same Type instead of each running the same emit. Faulted entries are evicted so the next caller retries.
  • Idempotent load — an assembly already present in the target context under that name is this compilation, so it is reused rather than reloaded. The assembly name now carries the full cache key instead of a 16-character prefix, making that reuse exact rather than probabilistic.
  • Loader recovery at the source — if LoadFromStream still throws FileLoadException, the context is rescanned once for an exact full-key match; without one the original exception is preserved.
  • Cache scope derived from the load context, folded into the cache key. This closes a latent correctness bug: MetadataReference.CreateFromImage(...).Display is null, so the helper assembly contributed nothing to the key — two helper sets exporting the same namespaces shared one entry, and the second flow silently executed the first flow's helpers.

The reproduction turned out broader than the original report: the same cold-compile race fires at the subflow output mapping (Instance:100030), the subflow input mapping (Instance:100023) and the auto-transition rule. The fix sits in the evaluator, so it covers all three. Measured with 30 concurrent starts on a helper-declaring parent/child pair: 28/30 stranded Busy before, 30/30 completed after.

ClearCache / InvalidateScript were removed from the evaluator — they would unload a live shared context.

Reference: PR #888 — see also Mappings.

Release pipeline repair (#886)

Shipped in v0.0.81.

The v0.0.80 release shipped images and a GitHub release but no NuGet packages, and the pipeline had no way to repair it. Four independent causes, all fixed:

  • NUGET_USER was read from a repository variable that was empty, so publish-nuget failed its own configuration guard. It is now a secret, read through env: rather than inlined, so the value stays masked and cannot be interpolated into the script.
  • A shipped release could not be re-published. Re-running the failed job picks up the workflow file from the original commit, and a fresh run walks to the first unused patch — so it would have produced 0.0.81 and left 0.0.80's packages permanently missing, with images on one version and packages on another. workflow_dispatch now honours the version input on the stable path. Re-publishing over a shipped tag is intentional but never implicit: it requires force_publish=true, the version is validated against X.Y.Z, and the push trigger is unchanged.
  • npm went red on a re-publish. npm publish fails hard on an already-published version and has no --skip-duplicate equivalent. The version is now checked against the registry first and the publish step is skipped rather than failed.
  • A dead link in every release summary — the summary linked the project name BBT.Workflow.Modules.Scripting, but the project packs as BBT.Workflow.Scripting, so the link pointed at an id that does not exist on nuget.org.

Reference: PR #886.

Busy manager isolated transactions (#890)

Shipped in v0.0.82.

Busy and release decisions read the ambient unit of work's state, which can be stale relative to the database when a concurrent request has already flipped the status. InstanceBusyManager now performs its authoritative status check inside an isolated transaction while the status lock is held, and persists and propagates the Busy state from that same isolated transaction — for every marking path, including the outcome-returning variant. Stale ambient state can no longer win a race, and no write or propagation is issued when nothing needs to change.

Reference: PR #890 — see also Transition pipeline.

Error-boundary transitions re-enter Busy instances (#892)

Shipped in v0.0.83 (tracked under milestone v0.0.82).

An error-boundary transition exists to recover an instance that is mid-execution — which is exactly when it is Busy. Admission was rejecting it under the ordinary Busy rule, so the boundary could not run and the instance stayed stuck. TransitionAdmissionService now classifies error-boundary transitions as admissible on a Busy instance, alongside the existing exemptions. Timer and retry re-entry behavior is unchanged.

Reference: PR #892 — see also Transition pipeline.


Behavior Changes

Two items from v0.0.84 change observable behavior, and both belong to the fail-closed query work (#881): instance queries that previously returned 200 with silently-widened results now return 400 (Validation:900011 / 900012), and the GetInstancesTask "-field" sort shorthand is no longer accepted — a task using it fails instead of running unfiltered. Both need a client- and definition-side audit before upgrading. The scheduledTransitions shape change from #894 is additive but bumps the response shape version to v7, invalidating cached bodies. The interim versions v0.0.81–v0.0.83 carry no breaking changes of their own. Each item, its impact direction and the migration steps are documented in the v0.0.84 breaking changes announcement.


Fixes

  • Elastic APM trace subtrees no longer detach — Dapr sidecar tracing is configured under the correct otel key with explicit protocol and isSecure, gRPC client instrumentation is registered, and duplicate sidecar state-store/lock spans are dropped at the collector (#887).
  • Concurrent script compilation no longer faults healthy instances — one compile per cache key, idempotent load, exact full-key assembly names, and a cache scope derived from the load context (#888).
  • Two helper sets exporting the same namespaces no longer share one cache entry — the second flow was silently executing the first flow's helpers (#888).
  • A failed release can be completed rather than abandoned — pinned-version dispatch behind force_publish, npm skip-if-published, and a corrected NuGet package link (#886).
  • Busy decisions read authoritative state — the status check and its write run in an isolated transaction under the status lock, so stale ambient unit-of-work state cannot win a race (#890).
  • Error-boundary transitions are admitted on Busy instances — recovery could not run on exactly the instances that needed it (#892).

Configuration Updates

Configuration for v0.0.84:

{
"runtimeVersion": "0.0.84",
"schemaVersion": "0.0.52"
}

Note: Schema version is unchanged at 0.0.52 across all four versions.

Dapr sidecar tracing must be authored under tracing.otel, with protocol and isSecure set explicitly. A configuration using the old otlp: key builds no exporter at all and reports no error, so sidecar spans never arrive and trace subtrees detach. Update every dapr/config.yaml — orchestration, execution, inbox and outbox — not only the ones mounted by your primary compose file.

Container images: published at tags 0.0.81, 0.0.82, 0.0.83 and 0.0.84 under ghcr.io/burgan-tech/vnext/*, Cosign-signed (keyless OIDC) with SBOM + provenance. Immutable digests are listed in each GitHub release: v0.0.81, v0.0.82, v0.0.83, v0.0.84.


Issues Referenced

  • vnext #881 — Reject unsupported instance query filters, sorts, groupings and aggregations.
  • vnext #894 — Add transition, view and schema link fields to scheduled transitions.
  • vnext #887 — Export the three missing links that detach a trace subtree in Elastic APM.
  • vnext #888 — Compile each script once per cache key and load it idempotently.
  • vnext #886 — Let a failed release be completed instead of skipped.
  • vnext #890 — Use isolated transactions for authoritative instance status checks.
  • vnext #892 — Admit error-boundary transitions on Busy instances.

Summary

  • Fail-closed instance queries (v0.0.84): unsupported filter / sort / groupBy / aggregation returns 400 (Validation:900011, 900012) instead of silently widening; GetInstancesTask fails rather than running unfiltered; the "-field" sort shorthand is gone.
  • scheduledTransitions gain link metadata (v0.0.84): href, view, schema with every capability reported unavailable; response shape v7.
  • Trace trees stay attached (v0.0.81): Dapr tracing.otel with protocol + isSecure, gRPC client instrumentation, sidecar duplicate filtering — 0 orphans on the business path.
  • Script compilation is race-free (v0.0.81): one compile per cache key, idempotent load, load-context-derived cache scope; fixes output-mapping, input-mapping and auto-rule variants.
  • Releases are repairable (v0.0.81): pinned-version re-publish behind force_publish, npm skip-if-published.
  • Busy reads are authoritative (v0.0.82) and error-boundary transitions are admitted on Busy instances (v0.0.83).
  • Schema stays at 0.0.52.

vNext Runtime Platform Team Released August 20, 2026