Skip to content
Settings

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.

  • 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.

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.

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.

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.

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.

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 annotations object.
  • Criteria carry a sibling criterionAnnotations object 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.

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 meta mirroring the canonical EntityMetadata; the obsolete previousTransition field is removed.
  • Error-code conformance โ€” the E2E validator now enforces documented errorCode strings for entity endpoints, not just response shapes.
  • ProblemDetail envelopes (application/problem+json) declared for the OIDC provider ops, audit search, async search, and the state-machine finished-event endpoint; the duplicate ProblemDetailDto schema was consolidated. getTechnicalUserToken keeps its RFC-6749 flat OAuth shape.
  • Config-conditional 501 now documented for the 21 IAM-gated operations when CYODA_IAM_MODE โ‰  jwt, plus 404 FEATURE_DISABLED on the trusted-key ops when that feature is off.
  • Uniform 404 MODEL_NOT_FOUND across 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-op pointInTime param, 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 new cyoda help messages topic documents the full edge-message surface.
  • Go toolchain 1.26.4 โ†’ 1.26.5 to clear govulncheck advisory GO-2026-5856.
  • goldmark v1.7.13 โ†’ v1.7.17 (indirect) to clear GO-2026-5320 (XSS in goldmark HTML rendering, reached via glamour in the cyoda help renderer). 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.

For installation guides, building from source, architecture, and running the engine locally or in production:

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!

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.