Cyoda-Go v0.8.2
Contract-true OpenAPI, richer entity queries, and transaction-joined compute callbacks.
Released 8 July 2026 ยท 10 issues delivered
This release sharpens the API contract and deepens the entity data layer. The OpenAPI spec was reconciled end-to-end so that what the document promises is exactly what the server does. Alongside that, entity reads and writes gain sorting, partial updates, composite uniqueness, and a unified point-in-time rule โ while compute-node callbacks can now join the workflow transaction that dispatched them.
โจ Highlights
Section titled โโจ Highlightsโ- Search result sorting: Order search results by any scalar data field or meta field, ascending or descending, with canonical cross-backend ordering.
- Partial entity updates (PATCH): Update only the fields that changed with RFC 7386 merge-patch semantics โ no more accidentally destroying fields you did not send.
- Composite unique keys: Declare multi-field uniqueness constraints on an entity model, enforced on create and update.
- Faster PostgreSQL search: Predicates now push down into SQL instead of loading and filtering every entity of a model in memory.
- Transaction-joined compute callbacks: Processor and criteria callbacks from a compute node now join the originating workflow transaction.
- Renderer annotations on processors & criteria: Attach display metadata to every workflow element for visualisers and condition builders.
- Contract-true OpenAPI: The spec was reconciled with the server โ error codes, response envelopes, and body shapes now match reality.
๐ Details
Section titled โ๐ Detailsโ๐ Search: sorting and pushdown
Section titled โ๐ Search: sorting and pushdownโBoth search endpoints
(POST /api/search/direct/{entityName}/{modelVersion} and the async
/api/search/async/{entityName}/{modelVersion} variant)
now accept sort query parameters (HTTP) or a structured orderBy array
(gRPC). The HTTP grammar is [@]path[:asc|desc] โ a bare dotted path for a
scalar data field, an @-prefixed name for a meta field (state,
creationDate, lastUpdateTime, transitionForLatestSave, transactionId,
id).
Ordering is canonical across every backend: Text by byte order, Numeric
by IEEE-754 double, Bool as false < true, and Temporal chronologically.
Absent or null values sort last, and entity_id is always the final
tiebreaker. Unsortable, array, or unknown paths are rejected with
400 INVALID_FIELD_PATH. The sort-key count is capped by
CYODA_SEARCH_MAX_SORT_KEYS (default 16).
Separately, PostgreSQL search now pushes supported predicates into SQL โ
JSONB extraction plus numeric, range, and string comparisons run in the
database, and LIMIT/OFFSET are pushed down when no residual filtering
remains. Non-pushable operators (regex, case-insensitive) are post-filtered
as rows stream. This eliminates full-model scans and per-document decode; it
is a constant-factor win, not a JSON-path-index feature (indexing queried
paths remains a separate operational step). SQLite already did this; the
memory backend filters in memory by design.
โ๏ธ Partial entity updates (PATCH)
Section titled โโ๏ธ Partial entity updates (PATCH)โA first-class partial-update capability closes a common data-loss footgun:
the existing PUT update has wholesale-replace semantics, so any field
omitted from the request was silently destroyed.
PATCH /api/entity/{format}/{entityId}PATCH /api/entity/{format}/{entityId}/{transition}The request body is a sparse JSON object applied to the stored entity
with RFC 7386 (application/merge-patch+json) semantics: a non-null key
overwrites, an explicit null deletes the key, and an omitted key is left
untouched. JSON-only (XML โ 415); RFC 6902 JSON Patch is recognised but
returns 501 for now.
Unlike PUT, If-Match is required โ the merge is applied relative to
the base you read, so patching a stale base risks a lost update. The token is
the meta.transactionId from your last GET. A missing If-Match returns
428 PRECONDITION_REQUIRED; a stale one returns 412. The merged result is
validated strictly against the model schema โ a PATCH never extends the
model, even in an extend-permitting mode.
๐ Composite unique keys
Section titled โ๐ Composite unique keysโEntity models can now declare one or more composite unique keys via
PUT /model/{entityName}/{modelVersion}/unique-keys (UNLOCKED models only).
Each key is an ordered set of scalar field paths; uniqueness is scoped to
(tenant, model, version) over live entities. The null rule is
all-or-nothing: all fields null or absent โ exempt; partial โ
422 INVALID_UNIQUE_KEY; all present โ enforced on create and update. String
comparison is byte-exact, and soft-delete frees the value-set. Supported by
the memory, sqlite, and postgres backends; the commercial backend returns
422 COMPOSITE_KEY_UNSUPPORTED until its own support lands.
New error codes: UNIQUE_VIOLATION (409), INVALID_UNIQUE_KEY (422),
COMPOSITE_KEY_UNSUPPORTED (422), INVALID_UNIQUE_KEY_DEFINITION (422).
๐๏ธ Conditional delete and point-in-time list reads
Section titled โ๐๏ธ Conditional delete and point-in-time list readsโDELETE /api/entity/{entityName}/{modelVersion} now honours an
AbstractConditionDto request body and deletes only matching entities (an
empty body still means all). This closes a data-loss defect where the
condition was ignored and the entire model was wiped. verbose=true returns
the deleted ids, and matched-vs-removed counts are reported separately. New
error code INVALID_CONDITION (400).
The model-scoped list read (getAllEntities) now honours pointInTime,
returning entities as-at the supplied instant and stamping meta.pointInTime.
โฑ๏ธ One point-in-time rule everywhere
Section titled โโฑ๏ธ One point-in-time rule everywhereโPoint-in-time (โas at Tโ) reads now apply a single canonical rule across
every storage engine and read path: inclusive of the requested instant
(<=), compared at native precision, with no millisecond round-up.
Previously the memory engine and the SQL GetAsAt/GetAllAsAt paths rounded
up to the next millisecond while sqlite used a strict < bound โ so
different backends, and even different read paths within one backend, could
disagree at sub-millisecond boundaries. That inconsistency is gone.
๐ Transaction-joined compute-node callbacks
Section titled โ๐ Transaction-joined compute-node callbacksโProcessor and criteria-evaluation callbacks from a compute node now join
the originating workflow transaction (T) rather than running standalone.
Before each dispatch the engine mints a signed HMAC tx-token and attaches it
to the outbound CloudEvent as the cyodatxtoken extension attribute. Compute
nodes echo it on callbacks (X-Tx-Token HTTP header / tx-token gRPC
metadata); the receiving node verifies the HMAC and routes the callback to
the transaction owner โ a local join when the owner is self, or an HTTP
reverse-proxy / gRPC forward otherwise.
Callbacks see the cascadeโs uncommitted writes, and their acks stay
provisional until T commits. ASYNC_NEW_TX callbacks join T via a
savepoint, so a processor failure discards its writes without aborting the
cascade. An absent token falls back to standalone execution โ the normal
behaviour for COMMIT_BEFORE_DISPATCH with startNewTxOnDispatch=false.
New env vars: CYODA_TX_TOKEN_TTL (token validity, default 90s),
CYODA_GRPC_NODE_ADDR (gRPC address advertised in tokens for BโA
forwarding), and CYODA_COMPUTE_HTTP_BASE (base URL for compute-test-client
HTTP callbacks).
๐งฉ Renderer annotations on processors and criteria
Section titled โ๐งฉ Renderer annotations on processors and criteriaโThe engine-ignored annotations bag โ previously available on workflows,
states, and transitions โ now extends to the two elements that lacked it:
- Processors carry an embedded
annotationsobject. - Criteria carry a sibling
criterionAnnotationsobject on the workflow and on each transition (the criterion tree itself round-trips verbatim and is never parsed to attach metadata).
Two well-known optional keys โ displayName and description โ are now
documented uniformly across all five element types for renderer and
condition-builder use. As before, annotations are object-only, capped at
64 KB per field, stored and re-emitted compacted, and never interpreted by
the engine; processor annotations are stripped before dispatch and never
reach compute members. This is an additive schema change: the workflow schema
moves to 1.2, and every existing 1.1 payload remains valid.
๐ OpenAPI contract reconciliation
Section titled โ๐ OpenAPI contract reconciliationโA substantial slice of this release makes the OpenAPI document tell the truth about the running server, so generated clients and conformance tools can trust it:
- Typed-but-open entity
metamirroring the canonicalEntityMetadata; the obsoletepreviousTransitionfield is removed. - Error-code conformance โ the E2E validator now enforces documented
errorCodestrings for entity endpoints, not just response shapes. ProblemDetailenvelopes (application/problem+json) declared for the OIDC provider ops, audit search, async search, and the state-machine finished-event endpoint; the duplicateProblemDetailDtoschema was consolidated.getTechnicalUserTokenkeeps its RFC-6749 flat OAuth shape.- Config-conditional
501now documented for the 21 IAM-gated operations whenCYODA_IAM_MODE โ jwt, plus404 FEATURE_DISABLEDon the trusted-key ops when that feature is off. - Uniform
404 MODEL_NOT_FOUNDacross every model-scoped read when the model is not registered for the tenant (these paths previously returned empty results silently). - Dead-surface disposition โ SQL-schema drift fixes, CQL cleanup, and a
marker-backing invariant; fictional contract surface (a phantom
403,408,timeoutMillis, a no-oppointInTimeparam, a bogus UUID constraint) removed. - Edge-message API โ the request envelopeโs optional metadata map is now
metaData(camelCase), symmetric with the response, and a newcyoda help messagestopic documents the full edge-message surface.
๐ก๏ธ Security
Section titled โ๐ก๏ธ Securityโ- Go toolchain 1.26.4 โ 1.26.5 to clear govulncheck advisory GO-2026-5856.
goldmarkv1.7.13 โ v1.7.17 (indirect) to clear GO-2026-5320 (XSS in goldmark HTML rendering, reached viaglamourin thecyoda helprenderer). The renderer only formats first-party help content embedded in the binary, so the advisory was never reachable with attacker-controlled input; the bump keeps the scan clean.
๐ Resources & getting started
Section titled โ๐ Resources & getting startedโFor installation guides, building from source, architecture, and running the engine locally or in production:
- Cyoda Hub (cyoda.dev) โ the primary portal for the Cyoda ecosystem, community links, and major updates.
- Cyoda Documentation (docs.cyoda.net) โ quick-starts, configuration schemas, API references, and operations guides.
- GitHub Project (github.com/Cyoda/cyoda-go) โ source code, build requirements, and binary releases with cryptographic checksums under the v0.8.2 release tag.
๐ฌ Feedback
Section titled โ๐ฌ FeedbackโFound a bug, hit a parity gap, or have a feature request? Open an issue or start a thread in our GitHub Discussions โ and feel free to join us on Discord. Thank you for building with Cyoda-Go!
๐ Footnote: why v0.8.2 is a patch, not a minor
Section titled โ๐ Footnote: why v0.8.2 is a patch, not a minorโThis release adds several genuinely new capabilities, yet it ships as a
patch. That is deliberate. Pre-1.0, we follow the widely-adopted convention
(Cargo, npm ^0.x) where the leftmost non-zero component is the de-facto
major and the minor component is reserved as a breaking-change signal.
Features ship in patches; a minor bump means โread the migration notesโ.
We also decoupled the cyoda-go-spi version from the binary version. The
two move independently now โ the SPI is pinned per release, but its number no
longer has to march in lock-step with the engine. See
docs/workflow-schema-versioning.md
and the README versioning section
for the full convention.
This release does still carry breaking contract changes โ see the breaking changes noted above โ but they are contract-tightening corrections, not a new feature epoch.