Skip to content
Settings

Working with entities

This page shows the patterns for interacting with entities through the platform API. Examples assume a local cyoda-go instance running on the default port with SQLite persistence; the same requests work against Cyoda Cloud with the cloud endpoint and an issued token.

The complete endpoint catalogue — parameters, response shapes, error codes — lives in the API reference. Keep that open as you work.

Cyoda speaks REST for CRUD, search, and workflow invocation, gRPC for external processors, and Trino SQL for analytics. This page covers REST; see Build → client compute nodes for gRPC and the APIs and surfaces overview for the decision framework.

Every request is authenticated with a bearer token. Every response includes the entity’s current revision, state, and timestamps.

For durable inbound payloads that are not yet entities — a staging buffer at the platform edge — see edge messages.

Post an entity to its model. The first time you post, Cyoda discovers the schema from what you send:

Terminal window
curl -X POST http://localhost:8080/api/entity/JSON/orders/1 \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{
"orderId": "ORD-42",
"customerId": "CUST-7",
"amount": 120.00,
"currency": "EUR",
"lines": [
{ "sku": "AX-1", "qty": 2, "price": 60.00 }
]
}'

The path is /api/entity/{format}/{entityName}/{modelVersion} — here JSON, orders, and version 1. The response carries an array whose first element contains entityIds[0], the system-assigned UUID of the new entity, plus its current state and revision number. Capture the UUID — downstream reads, updates, and transitions address the entity by that UUID, not by the business key orderId.

Fetch the current revision by id. The {entityId} in these URLs is the UUID returned in entityIds[0] from the create response, not a business key like orderId:

Terminal window
curl http://localhost:8080/api/entity/${ENTITY_ID} \
-H "Authorization: Bearer $TOKEN"

List every entity in a model with GET /api/entity/{entityName}/{modelVersion}:

GET /api/entity/orders/1

For filtered reads — predicates, pagination, result caps, historical reads — see searching entities. The list endpoint does not accept ad-hoc field filters; those belong to search.

Direct updates use PUT /api/entity/{format}/{entityId} (loopback update — stores a new revision without a named transition) or PUT /api/entity/{format}/{entityId}/{transition} (update with a named transition). PUT has wholesale-replace semantics: any field you omit from the body is destroyed. To change only some fields, use PATCH instead.

Mutations that move the entity between lifecycle states should go through a named transition, not a bare loopback update. Invoking the submit transition records it in the audit trail and runs any attached processors. The transition carries the new entity JSON in the request body (the platform stores the updated entity and records the named transition in one call):

Terminal window
curl -X PUT http://localhost:8080/api/entity/JSON/${ENTITY_ID}/submit \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{ "orderId": "ORD-42", "status": "submitted" }'

See Build → workflows and processors for how to declare transitions.

When you want to change only the fields that moved — without resending (and risking clobbering) the rest of the entity — use PATCH:

PATCH /api/entity/{format}/{entityId}
PATCH /api/entity/{format}/{entityId}/{transition}

The 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 that key, and an omitted key is left untouched.

Terminal window
curl -X PATCH http://localhost:8080/api/entity/JSON/${ENTITY_ID} \
-H 'Content-Type: application/merge-patch+json' \
-H "If-Match: ${LAST_TRANSACTION_ID}" \
-H "Authorization: Bearer $TOKEN" \
-d '{ "amount": 130.00, "note": null }'

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 header returns 428 PRECONDITION_REQUIRED and 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. The request is JSON-only (XML ⇒ 415); RFC 6902 JSON Patch is recognised but returns 501 for now.

Delete a single entity by id, or delete a matched subset of a model with a condition:

Terminal window
# Delete a single entity by id
curl -X DELETE http://localhost:8080/api/entity/${ENTITY_ID} \
-H "Authorization: Bearer $TOKEN"

DELETE /api/entity/{entityId} removes exactly the entity with that UUID and returns its id, model key, and the transaction id of the delete.

Terminal window
# Delete entities matching a condition (empty body ⇒ every entity in the model)
curl -X DELETE http://localhost:8080/api/entity/orders/1?verbose=true \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{
"type": "simple",
"jsonPath": "$.status",
"operation": "EQUALS",
"value": "cancelled"
}'

The DELETE /api/entity/{entityName}/{modelVersion} endpoint honours an AbstractConditionDto request body and removes only the matching entities; an empty body still means all. verbose=true returns the deleted ids, and the response reports matched-vs-removed counts separately. A malformed condition returns 400 INVALID_CONDITION.

Supplying the condition matters: earlier releases ignored it and wiped the entire model. Always send the condition you intend (or omit the body deliberately when you really do mean “all”).

Cyoda supports two query modes:

  • Direct (synchronous, capped result size) — API term direct. Returns right away. Good for UI lookups and short operations. Result size is capped, so direct is best for queries that produce a bounded, small result set.
  • Async (background, unbounded, paged) — API term async. Queued as a job, returns a handle you can poll. Result size is unbounded; results are paged. Good for large result sets, periodic reports, and exports. On the Cassandra-backed tier (Cyoda Cloud, or a licensed Enterprise install), async search runs distributed across the cluster and scales horizontally: query throughput for a fixed shape grows roughly linearly with the number of nodes.

Both accept the same filter grammar over entity fields, metadata, and workflow state. Pick direct by default; switch to async when a query would hit the direct result cap, would time out, or would hold resources you need elsewhere. For predicates, pagination, and worked examples, see searching entities.

Every entity’s history is queryable. Add a pointInTime parameter to any read or search request to retrieve the world as of that timestamp:

GET /api/entity/{entityId}?pointInTime=2026-03-01T00:00:00Z

Point-in-time reads 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. The model-scoped list read (GET /api/entity/{entityName}/{modelVersion}) also honours pointInTime and stamps meta.pointInTime on the result.

This is the primary way to answer regulatory and audit questions: what did this customer’s balance look like at quarter close? For the same parameter applied to searches, see searching entities → historical reads; for analytical reads expressed as SQL, see analytics with SQL.

When your code is reacting to a transition — running a processor or evaluating a criterion — talk to the platform over gRPC, not REST. The gRPC path preserves the audit association between the transition and the compute call, brokers identity, and supports streaming. See Build → client compute nodes for the implementation pattern.