Skip to content
Settings

Cyoda Calculation Member β€” Client Implementation Guide

A calculation member is an external gRPC client that participates in entity workflow processing on the Cyoda platform. The platform delegates work to your client over a persistent bidirectional gRPC stream, and your client returns results on the same stream.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” gRPC (bidirectional stream) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Cyoda Platform β”‚ ◄──────────────────────────────────────────►│ Your Calculation β”‚
β”‚ β”‚ CloudEvent (Protobuf, JSON payload) β”‚ Member (Client) β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚ Workflow Engineβ”‚ β”‚ 1. Client opens stream, sends Join β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ β”‚ β”‚ 2. Server responds with Greet β”‚ β”‚ Business Logic β”‚ β”‚
β”‚ β”‚ - Processors │──┼──3. Server pushes Processing/Criteria reqs──┼──│ β”‚ β”‚
β”‚ β”‚ - Criteria β”‚ β”‚ 4. Client returns responses β”‚ β”‚ - Data transforms β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ 5. Keep-alive heartbeats (bidirectional) β”‚ β”‚ - Criteria checks β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Two types of work can be delegated:

Use CaseDescriptionRequest TypeResponse Type
ProcessingPerform actions, such as transforming entity data during a workflow transition, performing CRUD ops on other entities, running reports, interacting with other systems, etc.EntityProcessorCalculationRequestEntityProcessorCalculationResponse
Criteria EvaluationEvaluate a boolean condition (e.g., β€œshould this transition fire?”)EntityCriteriaCalculationRequestEntityCriteriaCalculationResponse
  • Transport: gRPC bidirectional streaming via CloudEventsService.startStreaming
  • Message format: CNCF CloudEvents Protobuf envelope with JSON text_data payload
  • Authentication: Bearer JWT token in gRPC Authorization metadata header
  • Auth context propagation: The platform attaches CloudEvents Auth Context extension attributes to processor and criteria requests, identifying the principal whose action triggered the workflow (see Section 8)
  • Serialization: All payloads are JSON-serialized inside CloudEvent text_data (not binary protobuf)

Your client needs the following proto files to generate gRPC stubs:

  • cloudevents.proto β€” The standard CloudEvents Protobuf message definition (package io.cloudevents.v1)
  • cyoda-cloud-api.proto β€” The Cyoda service definition (package org.cyoda.cloud.api.grpc)

The service definition:

service CloudEventsService {
rpc startStreaming(stream io.cloudevents.v1.CloudEvent) returns (stream io.cloudevents.v1.CloudEvent);
}

The CloudEvent message:

message CloudEvent {
string id = 1; // Unique event ID (UUID recommended)
string source = 2; // URI-reference identifying the event source
string spec_version = 3; // Must be "1.0"
string type = 4; // Event type string (see Section 4)
map<string, CloudEventAttributeValue> attributes = 5;
oneof data {
bytes binary_data = 6;
string text_data = 7; // ← Used by Cyoda (JSON payload)
google.protobuf.Any proto_data = 8;
}
}

Obtain a valid JWT Bearer token from the Cyoda IAM system (OAuth 2.0 client credentials flow). The token must contain:

  • A valid caas_org_id claim (your legal entity ID)
  • Valid user roles

The token is validated on every stream establishment. If the token expires during an active stream, the stream remains valid β€” re-authentication occurs only when a new stream is opened.

For JVM-based clients, the recommended dependencies are:

  • io.grpc:grpc-stub, io.grpc:grpc-protobuf, io.grpc:grpc-netty-shaded β€” gRPC runtime
  • io.cloudevents:cloudevents-protobuf β€” CloudEvents SDK Protobuf format support
  • io.cloudevents:cloudevents-core β€” CloudEvents SDK core
  • com.fasterxml.jackson.core:jackson-databind β€” JSON serialization

ManagedChannel channel = ManagedChannelBuilder
.forAddress("cyoda-host.example.com", 50051)
.usePlaintext() // Use .useTransportSecurity() for TLS in production
.keepAliveTime(30, TimeUnit.SECONDS)
.keepAliveTimeout(10, TimeUnit.SECONDS)
.build();

Production TLS: In production, always use TLS. Replace .usePlaintext() with:

.useTransportSecurity()
.sslContext(/* your SSL context */)

Create a CallCredentials implementation that injects the Authorization header:

CallCredentials callCredentials = new CallCredentials() {
@Override
public void applyRequestMetadata(RequestInfo requestInfo, Executor executor, MetadataApplier applier) {
executor.execute(() -> {
Metadata headers = new Metadata();
headers.put(
Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER),
"Bearer " + jwtTokenSupplier.get() // Always fetch a fresh token
);
applier.apply(headers);
});
}
};
CloudEventsServiceGrpc.CloudEventsServiceStub asyncStub = CloudEventsServiceGrpc
.newStub(channel)
.withCallCredentials(callCredentials)
.withWaitForReady(); // Wait for the channel to become ready before sending

Every message on the stream is a CloudEvent with a type field that determines how to deserialize the JSON text_data. Your client must handle the following types:

CloudEvent typeDirectionDescription
CalculationMemberJoinEventClient β†’ ServerRegister as a calculation member
CalculationMemberGreetEventServer β†’ ClientServer confirms registration
CalculationMemberKeepAliveEventBidirectionalHeartbeat probe and response
EventAckResponseServer β†’ ClientAcknowledgment of keep-alive
EntityProcessorCalculationRequestServer β†’ ClientProcess entity data
EntityProcessorCalculationResponseClient β†’ ServerReturn processed entity data
EntityCriteriaCalculationRequestServer β†’ ClientEvaluate a boolean criterion
EntityCriteriaCalculationResponseClient β†’ ServerReturn criterion result

To send a CloudEvent on the stream (Java/Kotlin with CloudEvents SDK):

// 1. Build the CloudEvents SDK event
io.cloudevents.CloudEvent sdkEvent = CloudEventBuilder.v1()
.withType("CalculationMemberJoinEvent") // Must match the type table above
.withSource(URI.create("my-calculation-member"))
.withId(UUID.randomUUID().toString())
.withData(PojoCloudEventData.wrap(event, e -> objectMapper.writeValueAsBytes(e)))
.build();
// 2. Serialize to Protobuf
EventFormat protobufFormat = EventFormatProvider.getInstance()
.resolveFormat("application/cloudevents+protobuf");
byte[] protoBytes = protobufFormat.serialize(sdkEvent);
// 3. Parse to the gRPC CloudEvent message
io.cloudevents.v1.proto.CloudEvent grpcEvent =
io.cloudevents.v1.proto.CloudEvent.parseFrom(protoBytes);
// From the gRPC StreamObserver<CloudEvent>.onNext(value):
String eventType = value.getType();
String jsonPayload = value.getTextData();
// Deserialize based on type
switch (eventType) {
case "CalculationMemberGreetEvent":
GreetEvent greet = objectMapper.readValue(jsonPayload, GreetEvent.class);
break;
case "EntityProcessorCalculationRequest":
ProcessorRequest req = objectMapper.readValue(jsonPayload, ProcessorRequest.class);
break;
// ... etc
}

StreamObserver<CloudEvent> requestObserver = asyncStub.startStreaming(
new StreamObserver<CloudEvent>() {
@Override
public void onNext(CloudEvent value) {
// Dispatch based on value.getType() β€” see Sections 6–8
}
@Override
public void onError(Throwable t) {
// Connection lost β€” trigger reconnect (see Section 11)
}
@Override
public void onCompleted() {
// Server closed the stream β€” trigger reconnect
}
}
);

Immediately after opening the stream, send a CalculationMemberJoinEvent:

{
"id": "<uuid>",
"tags": ["my-processor-tag", "production"]
}

Tags are critical for routing. The platform routes processing/criteria requests to members whose tags are a superset of the tags configured on the workflow processor/criterion. Tags are case-insensitive (lowercased server-side).

The server responds with a CalculationMemberGreetEvent:

{
"id": "<uuid>",
"success": true,
"memberId": "<server-assigned-member-uuid>",
"joinedLegalEntityId": "<your-legal-entity-id>"
}

Store the memberId β€” you will need it for keep-alive messages.

If success is false, inspect the error object for the failure reason (e.g., subscription limit exceeded, invalid token).

The platform periodically probes your member with CalculationMemberKeepAliveEvent messages to verify liveness. You must respond to each probe with an EventAckResponse.

Server-initiated keep-alive probe (Server β†’ Client):

{
"id": "<probe-uuid>",
"memberId": "<your-member-id>"
}

Required response (Client β†’ Server):

{
"id": "<new-uuid>",
"sourceEventId": "<probe-uuid>",
"success": true
}

You may also send client-initiated keep-alive messages to confirm your own liveness. The server will respond with an EventAckResponse.

Timing parameters (server-side defaults):

ParameterDefaultDescription
Keep-alive probe interval1,000 msHow often the server probes
Max idle interval3,000 msHow long before a member is marked as not alive
Keep-alive check timeout1,000 msHow long the server waits for a probe response

If your member is marked as not alive, the platform will not route requests to it. The member remains registered but idle. Responding to a subsequent keep-alive probe restores the alive status.

⚠️ Critical: Failing to respond to keep-alive probes will cause your member to be marked as dead. Ensure your keep-alive response handler is fast and non-blocking.


When an entity reaches a workflow transition with an externalized processor configured to match your member’s tags, the platform sends an EntityProcessorCalculationRequest.

{
"id": "<event-uuid>",
"requestId": "<correlation-id>",
"entityId": "<entity-uuid>",
"processorId": "<processor-uuid>",
"processorName": "<configured-processor-name>",
"transactionId": "<transaction-uuid>",
"workflow": {
"id": "<workflow-uuid>",
"name": "<workflow-name>"
},
"transition": {
"id": "<transition-uuid>",
"name": "<transition-name>",
"stateFrom": "<source-state>",
"stateTo": "<target-state>"
},
"parameters": { /* arbitrary JSON configured on the processor */ },
"payload": {
"type": "TREE",
"data": { /* entity data as JSON β€” present only if attachEntity=true */ },
"meta": { /* entity metadata */ }
}
}

Key fields:

  • requestId β€” You must echo this back in the response for correlation.
  • entityId β€” The entity being processed. Echo this back.
  • processorName β€” Use this to dispatch to different business logic handlers.
  • parameters β€” Arbitrary JSON configured on the processor in the workflow definition (the context field). Use for passing configuration to your handler.
  • payload.data β€” The entity data. Only present when attachEntity is true in the workflow configuration.

πŸ’‘ Auth context: The CloudEvent envelope for this request also carries auth context extension attributes (authtype, authid, authclaims) identifying the principal whose action triggered the workflow. See Section 8 for details on how to extract them.

{
"id": "<new-uuid>",
"requestId": "<echo-request-id>",
"entityId": "<echo-entity-id>",
"success": true,
"payload": {
"type": "TREE",
"data": { /* modified entity data to write back */ }
}
}

Rules:

  1. requestId must exactly match the value from the request.
  2. entityId must exactly match the value from the request.
  3. If you set success: true, the platform applies your payload.data to the entity.
  4. If you set success: false, the platform treats this as a processing failure. Include an error object.
  5. The payload field is optional. If omitted (or payload.data is null), no data modification occurs.
{
"id": "<new-uuid>",
"requestId": "<echo-request-id>",
"entityId": "<echo-entity-id>",
"success": false,
"error": {
"code": "BUSINESS_ERROR",
"message": "Detailed error description",
"retryable": true
}
}

The error.retryable flag tells the platform whether it should retry the request on a different member (if a retry policy is configured). Set to true for transient failures and false for permanent failures.


When a workflow transition has an externalized criterion configured as a function, the platform sends an EntityCriteriaCalculationRequest.

{
"id": "<event-uuid>",
"requestId": "<correlation-id>",
"entityId": "<entity-uuid>",
"criteriaId": "<criteria-uuid>",
"criteriaName": "<configured-function-name>",
"target": "TRANSITION",
"transactionId": "<transaction-uuid>",
"workflow": {
"id": "<workflow-uuid>",
"name": "<workflow-name>"
},
"transition": {
"id": "<transition-uuid>",
"name": "<transition-name>",
"stateFrom": "<source-state>",
"stateTo": "<target-state>"
},
"processor": {
"id": "<processor-uuid>",
"name": "<processor-name>"
},
"parameters": { /* arbitrary JSON */ },
"payload": {
"type": "TREE",
"data": { /* entity data */ }
}
}

The target field indicates what the criterion is attached to:

TargetMeaningAvailable Context
WORKFLOWWorkflow-level criterion (selects which workflow applies)workflow
TRANSITIONTransition-level criterion (should this transition fire?)workflow, transition
PROCESSORProcessor-level criterion (should this processor run?)workflow, transition, processor
NAReserved for future useβ€”

πŸ’‘ Auth context: Like processor requests, criteria requests also carry auth context extension attributes on the CloudEvent envelope. See Section 8.

{
"id": "<new-uuid>",
"requestId": "<echo-request-id>",
"entityId": "<echo-entity-id>",
"success": true,
"matches": true,
"reason": "Entity meets all validation criteria"
}

Key fields:

  • requestId β€” Must exactly match the request.
  • entityId β€” Must exactly match the request.
  • matches β€” The boolean result: true means the criterion is satisfied (transition fires / processor runs), false means it is not.
  • reason β€” Optional human-readable explanation (useful for debugging).

If success: false, the platform treats it as a criteria evaluation failure (the criterion evaluates to false by default).


The platform attaches CloudEvents Auth Context extension attributes to every EntityProcessorCalculationRequest and EntityCriteriaCalculationRequest. These attributes identify the authenticated principal whose action triggered the workflow execution (e.g., the user who created or updated the entity).

The auth context is carried as CloudEvent extension attributes in the Protobuf attributes map β€” not inside the JSON text_data payload.

AttributeTypeRequiredDescription
authtypeStringYESPrincipal type. One of: user, service_account, system, unauthenticated, unknown
authidStringNOUnique identifier of the principal (UUID). Absent for system or unauthenticated.
authclaimsStringNOJSON string containing claims about the principal (e.g., legalEntityId, roles). Does not contain credentials.
authtype ValueMeaning
userA regular authenticated user (JWT-based login)
service_accountA machine-to-machine (M2M) technical user
systemAn internal platform trigger (no user context, e.g., scheduled transitions)
unauthenticatedNo authentication context was available
unknownReserved for future use

The attributes are available in the Protobuf CloudEvent’s attributes map. The keys are the attribute names listed above (no prefix):

// From the gRPC StreamObserver<CloudEvent>.onNext(value):
String authType = value.getAttributesMap().get("authtype").getCeString();
String authId = value.getAttributesMap().containsKey("authid")
? value.getAttributesMap().get("authid").getCeString()
: null;
String authClaimsJson = value.getAttributesMap().containsKey("authclaims")
? value.getAttributesMap().get("authclaims").getCeString()
: null;
// Parse claims if present
if (authClaimsJson != null) {
Map<String, Object> claims = objectMapper.readValue(authClaimsJson, Map.class);
String legalEntityId = (String) claims.get("legalEntityId");
List<String> roles = (List<String>) claims.get("roles"); // may be null for plain IUser
}
{
"legalEntityId": "acme-corp",
"roles": ["USER", "SUPER_USER"]
}

For service_account (M2M) users:

{
"legalEntityId": "acme-corp",
"roles": ["M2M"]
}
  • Audit logging: Record which user triggered the processing for compliance.
  • Authorization decisions: Apply different business logic based on the caller’s roles or legal entity.
  • Multi-tenant isolation: Verify the triggering principal belongs to the expected tenant.
  • Debugging: Trace processing failures back to the originating user action.

⚠️ Note: The authclaims field never contains credentials (passwords, tokens, secrets). It contains only identity and authorization metadata.


Your calculation member does not exist in isolation β€” it is invoked by workflow configurations on the platform side. This section describes how workflows reference externalized processors and criteria, so you understand the relationship between your member’s tags/handlers and the platform configuration.

{
"workflows": [{
"version": "1.0",
"name": "my-workflow",
"initialState": "start",
"states": {
"start": {
"transitions": [{
"name": "process-data",
"next": "processed",
"manual": false,
"processors": [{
"name": "my-processor-function",
"executionMode": "SYNC",
"config": {
"attachEntity": true,
"calculationNodesTags": "my-processor-tag",
"responseTimeoutMs": 60000,
"retryPolicy": "FIXED",
"context": "{\"key\": \"value\"}"
}
}]
}]
},
"processed": {}
}
}]
}
FieldTypeDefaultDescription
namestringβ€”Required. The processor name. Sent as processorName in the request.
executionModestringβ€”Required. One of SYNC, ASYNC_SAME_TX, ASYNC_NEW_TX.
config.attachEntitybooleantrueWhether to send entity data in the request payload.
config.calculationNodesTagsstring""Comma/semicolon-separated tags. Only members whose tags are a superset are eligible.
config.responseTimeoutMslong60000How long the platform waits for your response before timing out.
config.retryPolicystringFIXEDNONE β€” no retry. FIXED β€” retry with fixed delay (default: 3 retries, 500ms delay).
config.contextstringnullArbitrary string passed as parameters in the request. Use for handler-specific configuration.
config.asyncResultbooleanfalseEnable async response processing (advanced).
config.crossoverToAsyncMslong5000Time before switching from sync to async response handling (advanced).
ModeBehavior
SYNCThe workflow engine waits for your response within the same transaction. The transition completes only after your response is applied.
ASYNC_SAME_TXThe engine sends the request and can process other work. Your response is applied within the same entity transaction.
ASYNC_NEW_TXLike ASYNC_SAME_TX, but your response is applied in a new transaction. Useful for long-running computations.

For most use cases, SYNC is the simplest and recommended starting point.

{
"transitions": [{
"name": "conditional-transition",
"next": "target-state",
"manual": false,
"criterion": {
"type": "function",
"function": {
"name": "my-criteria-function",
"config": {
"attachEntity": true,
"calculationNodesTags": "my-processor-tag",
"responseTimeoutMs": 5000,
"retryPolicy": "NONE"
}
}
}
}]
}

Criteria functions use the same config fields as processors (except asyncResult and crossoverToAsyncMs, which are not applicable to criteria).

PolicyBehavior
NONENo retry. If the member fails or times out, the processing fails.
FIXEDRetries up to N times (default: 3) with a fixed delay (default: 500ms) between retries. Each retry attempts a different member if available (the failed member is excluded from selection).

All events on the stream extend the BaseEvent schema:

{
"id": "<string, required>",
"success": true,
"error": {
"code": "<string, required if error>",
"message": "<string, required if error>",
"retryable": false
},
"warnings": ["<optional array of warning strings>"]
}
  • id β€” Every event must have a unique ID (UUID recommended).
  • success β€” Defaults to true. Set to false to indicate an error.
  • error β€” Only relevant when success is false. The code and message fields are required within the error object.
  • warnings β€” Optional array of warning strings.

gRPC streams can be terminated by network issues, server restarts, or load balancer timeouts. Implement automatic reconnection:

  1. Detect disconnection via onError or onCompleted on the response observer.
  2. Back off exponentially β€” start at 1 second, cap at 60 seconds.
  3. Re-join after reconnect β€” every new stream requires a fresh CalculationMemberJoinEvent.
  4. Refresh the JWT token before reconnecting if it is near expiry.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” onError/onCompleted β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” delay β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” success β”Œβ”€β”€β”€β”€β”€β”€β”
β”‚ Connectedβ”‚ ──────────────────────► β”‚ Backoff β”‚ ────────► β”‚ Reconnecting β”‚ ────────────► β”‚ Join β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”˜
β–² β”‚ failure β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Backoff β”‚ (increase delay) β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Greet received

The gRPC StreamObserver is not thread-safe. If your business logic runs on multiple threads, synchronize all calls to observer.onNext():

synchronized (requestObserver) {
requestObserver.onNext(cloudEvent);
}

Your client must respond within the configured responseTimeoutMs (default: 60 seconds). If you exceed this:

  • The platform considers the request failed.
  • If retry policy is FIXED, the platform retries with a different member.
  • Late responses are silently discarded.

Design your business logic to complete well within the timeout, accounting for network latency.

In edge cases (e.g., network partitions, retries), you may receive the same request more than once. Use the requestId as an idempotency key to avoid processing the same request twice.

When shutting down your client:

  1. Stop accepting new requests (drain in-flight work).
  2. Complete any pending responses and send them.
  3. Close the gRPC stream via requestObserver.onCompleted().
  4. Shut down the ManagedChannel with a grace period:
    channel.shutdown().awaitTermination(10, TimeUnit.SECONDS);

The platform will detect the stream closure and broadcast a member-offline event to the cluster. Pending requests that were in-flight will time out and may be retried on other members.

You can run multiple calculation member instances (same or different processes) with the same tags for horizontal scaling and high availability. The platform selects one eligible member per request, preferring members connected to the local cluster node. Running at least two members ensures continued processing if one goes down.

Track these metrics in your client:

  • Request count by type (processor vs. criteria) and result (success vs. failure)
  • Response latency (time from receiving request to sending response)
  • Keep-alive response time
  • Reconnection count and frequency
  • Stream errors (by gRPC status code)

Client Server
β”‚ β”‚
│──── startStreaming() ─────────────────────────►│ (open bidirectional stream)
β”‚ β”‚
│──── CalculationMemberJoinEvent ───────────────►│ (register with tags)
│◄─── CalculationMemberGreetEvent ───────────────│ (server confirms, assigns memberId)
β”‚ β”‚
│◄─── CalculationMemberKeepAliveEvent ───────────│ (periodic heartbeat probe)
│──── EventAckResponse ─────────────────────────►│ (ack the probe)
β”‚ β”‚
│◄─── EntityProcessorCalculationRequest ─────────│ (process this entity)
│──── EntityProcessorCalculationResponse ───────►│ (here's the result)
β”‚ β”‚
│◄─── EntityCriteriaCalculationRequest ──────────│ (evaluate this criterion)
│──── EntityCriteriaCalculationResponse ────────►│ (matches: true/false)
β”‚ β”‚
│──── CalculationMemberKeepAliveEvent ──────────►│ (client-initiated heartbeat)
│◄─── EventAckResponse ─────────────────────────│ (server acks)
β”‚ β”‚

SymptomLikely CauseFix
UNAUTHENTICATED on stream openMissing/invalid/expired JWT tokenRefresh JWT before connecting. Ensure Authorization: Bearer <token> header.
NOT_FOUND after JWT validationUser not found in Cyoda for the given JWTVerify user enrollment and legal entity configuration.
Greet event has success: falseSubscription limit exceeded (max client nodes)Check your subscription plan limits.
Member marked as not aliveKeep-alive responses too slow or missingEnsure non-blocking, fast keep-alive handler. Check network latency.
Requests not arrivingTags mismatchVerify your member’s tags are a superset of the workflow processor’s calculationNodesTags. Tags are case-insensitive.
Requests not arrivingMember on wrong legal entityRequests only route to members in the same legal entity as the entity owner.
Request timeoutBusiness logic too slowOptimize processing time or increase responseTimeoutMs in workflow config.
Duplicate requestsRetry policy triggeredImplement idempotency using requestId.
Stream drops unexpectedlyServer restart, network issue, idle timeoutImplement reconnection with exponential backoff (Section 11.1).
authtype is system unexpectedlyWorkflow triggered by an internal platform action (e.g., scheduled transition) or no user context was availableThis is expected for system-initiated workflows. If you expect a user context, verify the originating API call is authenticated.
authclaims is missingThe triggering principal is a plain IUser without extended claims, or the auth type is system/unauthenticatedOnly user and service_account auth types include claims. Check authtype before parsing claims.