{
  "topic": "grpc",
  "path": [
    "grpc"
  ],
  "title": "grpc — gRPC service contract",
  "synopsis": "cyoda-go exposes one gRPC service: `CloudEventsService` (package `org.cyoda.cloud.api.grpc`). All gRPC methods use the CloudEvents Protobuf envelope (`io.cloudevents.v1.CloudEvent`) as both request and response types. The event type string in the CloudEvent envelope selects the operation; the JSON payload in `text_data` (or `binary_data`) carries the operation-specific body.",
  "body": "# grpc\n\n## NAME\n\ngrpc — gRPC service contract for compute members and entity management.\n\n## SYNOPSIS\n\n```\ngrpcurl -plaintext localhost:9090 list\ngrpcurl -plaintext localhost:9090 org.cyoda.cloud.api.grpc.CloudEventsService/StartStreaming\n```\n\n## DESCRIPTION\n\ncyoda-go exposes one gRPC service: `CloudEventsService` (package `org.cyoda.cloud.api.grpc`). All gRPC methods use the CloudEvents Protobuf envelope (`io.cloudevents.v1.CloudEvent`) as both request and response types. The event type string in the CloudEvent envelope selects the operation; the JSON payload in `text_data` (or `binary_data`) carries the operation-specific body.\n\nThe primary use case is the compute member protocol: external workflow processors subscribe via `StartStreaming`, receive processor and criteria calculation requests, and respond over the same bidirectional stream.\n\nThe secondary use case is programmatic entity and model management: `entityManage`, `entityManageCollection`, `entityModelManage`, `entitySearch`, and `entitySearchCollection` allow gRPC clients to perform the same CRUD operations as the REST API.\n\n## CONNECTION\n\n**Endpoint**: `host:CYODA_GRPC_PORT` (default `localhost:9090`).\n\n**Transport**: plaintext TCP by default. TLS termination is handled by the ingress or service mesh in production deployments.\n\n**Authentication**: Bearer token passed as gRPC metadata key `authorization`. The value is the same `Bearer <token>` string as used in the HTTP API. Both mock IAM and JWT modes apply identically to gRPC connections — the auth interceptor extracts the `authorization` metadata value, builds an `http.Request` with that `Authorization` header, and delegates to the configured `AuthenticationService`.\n\n**OTel tracing**: when `CYODA_OTEL_ENABLED=true`, the gRPC server installs an `otelgrpc.NewServerHandler()` stats handler that creates spans for every inbound RPC.\n\n## SERVICES\n\nThe proto package is `org.cyoda.cloud.api.grpc`. The Go package is `github.com/cyoda-platform/cyoda-go/api/grpc/cyoda`.\n\n```proto\nsyntax = \"proto3\";\n\npackage org.cyoda.cloud.api.grpc;\n\nimport \"cloudevents/cloudevents.proto\";\n\nservice CloudEventsService {\n  rpc startStreaming(stream io.cloudevents.v1.CloudEvent)\n      returns (stream io.cloudevents.v1.CloudEvent);\n\n  rpc entityModelManage(io.cloudevents.v1.CloudEvent)\n      returns (io.cloudevents.v1.CloudEvent);\n\n  rpc entityManage(io.cloudevents.v1.CloudEvent)\n      returns (io.cloudevents.v1.CloudEvent);\n\n  rpc entityManageCollection(io.cloudevents.v1.CloudEvent)\n      returns (stream io.cloudevents.v1.CloudEvent);\n\n  rpc entitySearch(io.cloudevents.v1.CloudEvent)\n      returns (io.cloudevents.v1.CloudEvent);\n\n  rpc entitySearchCollection(io.cloudevents.v1.CloudEvent)\n      returns (stream io.cloudevents.v1.CloudEvent);\n}\n```\n\n**startStreaming** — bidirectional streaming RPC for compute member lifecycle. Requires `ROLE_M2M`. First message must be `CalculationMemberJoinEvent`. Server sends processor and criteria requests; client sends responses and keep-alive acknowledgments.\n\n**entityModelManage** — unary RPC for entity model operations. Accepts: `EntityModelImportRequest`, `EntityModelExportRequest`, `EntityModelTransitionRequest`, `EntityModelDeleteRequest`, `EntityModelGetAllRequest`.\n\n**entityManage** — unary RPC for single-entity operations. Accepts: `EntityCreateRequest`, `EntityUpdateRequest`, `EntityDeleteRequest`, `EntityDeleteAllRequest`, `EntityTransitionRequest`.\n\n**entityManageCollection** — server-streaming RPC for batch entity operations. Accepts: `EntityCreateCollectionRequest`, `EntityUpdateCollectionRequest`. Streams one response CloudEvent per entity.\n\n**entitySearch** — unary RPC for entity retrieval. Accepts: `EntityGetRequest`, `EntityGetAllRequest`, `EntitySnapshotSearchRequest`, `EntitySearchRequest`, `SnapshotCancelRequest`, `SnapshotGetRequest`, `SnapshotGetStatusRequest`, `EntityStatsGetRequest`, `EntityStatsByStateGetRequest`, `EntityChangesMetadataGetRequest`.\n\n**entitySearchCollection** — server-streaming RPC for collection retrieval. Streams results.\n\n## MESSAGE TYPES\n\nAll CloudEvents are encoded in the Protobuf CloudEvent format. The `type` field selects the operation. The `text_data` field carries the JSON-encoded payload.\n\n**CloudEvent envelope** (`io.cloudevents.v1.CloudEvent`):\n\n```proto\nmessage CloudEvent {\n  string id = 1;          // UUID\n  string source = 2;      // \"cyoda\"\n  string spec_version = 3; // \"1.0\"\n  string type = 4;         // event type constant\n\n  map<string, CloudEventAttributeValue> attributes = 5;\n\n  oneof data {\n    bytes  binary_data = 6;\n    string text_data   = 7;   // JSON payload\n    google.protobuf.Any proto_data = 8;\n  }\n}\n```\n\n**Streaming event types** (compute member protocol):\n\n- `CalculationMemberJoinEvent` — first message from client; registers the member\n- `CalculationMemberGreetEvent` — server response to join; includes assigned member ID\n- `CalculationMemberKeepAliveEvent` — bidirectional; server sends on interval, client echoes\n- `EntityProcessorCalculationRequest` — server → client; processor dispatch request\n- `EntityProcessorCalculationResponse` — client → server; processor result\n- `EntityCriteriaCalculationRequest` — server → client; criteria dispatch request\n- `EntityCriteriaCalculationResponse` — client → server; criteria result\n- `EventAckResponse` — client → server; acknowledges any server event\n\n**EventAckResponse `text_data` JSON shape:**\n\n```json\n{\n  \"id\": \"<uuid for this ack message>\",\n  \"sourceEventId\": \"<id of the server event being acknowledged>\",\n  \"success\": true,\n  \"warnings\": [],\n  \"error\": null\n}\n```\n\nFields:\n- `id` (string, required) — unique identifier for this ack message; any UUID\n- `sourceEventId` (string, required) — the `id` field from the server CloudEvent being acknowledged\n- `success` (boolean, optional, default `true`) — set to `true` for a normal ack; `false` if the client is reporting a processing error\n- `warnings` (string array, optional) — diagnostic messages; may be omitted\n- `error` (object, optional) — present only when `success=false`; shape: `{\"code\":\"<code>\",\"message\":\"<msg>\",\"retryable\":<bool|null>}`\n\nThe full CloudEvent envelope for an ack:\n\n```json\n{\n  \"id\": \"<ack-uuid>\",\n  \"source\": \"client\",\n  \"spec_version\": \"1.0\",\n  \"type\": \"EventAckResponse\",\n  \"text_data\": \"{\\\"id\\\":\\\"<ack-uuid>\\\",\\\"sourceEventId\\\":\\\"<server-event-id>\\\",\\\"success\\\":true}\"\n}\n```\n\n`EventAckResponse` updates the member's last-seen timestamp, preventing keep-alive timeout. It is used to acknowledge any server event for which the client has no substantive response (e.g. a keep-alive or a greet event).\n\n**Entity management event types**:\n\n- `EntityCreateRequest` / `EntityTransactionResponse`\n- `EntityCreateCollectionRequest` / `EntityTransactionResponse` (streamed)\n- `EntityUpdateRequest` / `EntityTransactionResponse`\n- `EntityUpdateCollectionRequest` / `EntityTransactionResponse` (streamed)\n- `EntityDeleteRequest` / `EntityDeleteResponse`\n- `EntityDeleteAllRequest` / `EntityDeleteAllResponse`\n- `EntityTransitionRequest` / `EntityTransitionResponse`\n\n**Model management event types**:\n\n- `EntityModelImportRequest` / `EntityModelImportResponse`\n- `EntityModelExportRequest` / `EntityModelExportResponse`\n- `EntityModelTransitionRequest` / `EntityModelTransitionResponse`\n- `EntityModelDeleteRequest` / `EntityModelDeleteResponse`\n- `EntityModelGetAllRequest` / `EntityModelGetAllResponse`\n\n**Search / query event types**:\n\n- `EntityGetRequest` / `EntityResponse`\n- `EntityGetAllRequest` / `EntityResponse` (streamed via entitySearchCollection)\n- `EntitySnapshotSearchRequest` / `EntitySnapshotSearchResponse`\n- `EntitySearchRequest` / `EntityResponse`\n- `SnapshotCancelRequest` / `EntitySnapshotSearchResponse`\n- `SnapshotGetRequest` / `EntitySnapshotSearchResponse`\n- `SnapshotGetStatusRequest` / `EntitySnapshotSearchResponse`\n- `EntityStatsGetRequest` / `EntityStatsResponse`\n- `EntityStatsByStateGetRequest` / `EntityStatsByStateResponse`\n- `EntityChangesMetadataGetRequest` / `EntityChangesMetadataResponse`\n\n## COMPUTE MEMBER PROTOCOL\n\nThe compute member protocol allows external processes to serve as workflow processor and criteria nodes.\n\n**Join sequence:**\n\n1. Client opens `startStreaming` with `Authorization: Bearer <token>` metadata. Token must carry `ROLE_M2M`.\n2. Client sends `CalculationMemberJoinEvent` as the first message:\n\n```json\n{\n  \"id\": \"<uuid>\",\n  \"tags\": [\"approval-service\", \"notification\"],\n  \"joinedLegalEntityId\": \"acme-corp\"\n}\n```\n\n`joinedLegalEntityId` must match the tenant ID in the bearer token. When present and mismatched, the server returns `codes.PermissionDenied`. When absent, the server uses the token's tenant ID implicitly. Include `joinedLegalEntityId` in all join messages — clients that omit it against a strict server may fail if validation is tightened.\n\n3. Server registers the member and responds with `CalculationMemberGreetEvent`:\n\n```json\n{\n  \"id\": \"<server-assigned-member-uuid>\",\n  \"memberId\": \"<server-assigned-member-uuid>\",\n  \"joinedLegalEntityId\": \"<tenantId>\",\n  \"success\": true\n}\n```\n\n**Processor dispatch (server → client):**\n\nServer sends `EntityProcessorCalculationRequest` when a workflow transition invokes an `EXTERNAL` processor whose `calculationNodesTags` matches one of the member's declared tags:\n\n```json\n{\n  \"id\": \"<requestId>\",\n  \"requestId\": \"<requestId>\",\n  \"entityId\": \"<entityUUID>\",\n  \"processorId\": \"notify-approval\",\n  \"processorName\": \"notify-approval\",\n  \"workflow\": {\"id\": \"prize-lifecycle\", \"name\": \"prize-lifecycle\"},\n  \"transition\": {\"id\": \"APPROVE\", \"name\": \"APPROVE\"},\n  \"transactionId\": \"<txUUID>\",\n  \"success\": true,\n  \"payload\": {\n    \"type\": \"JSON\",\n    \"data\": {<entity JSON body>},\n    \"meta\": {\n      \"id\": \"<entityUUID>\",\n      \"modelKey\": {\"name\": \"nobel-prize\", \"version\": 1},\n      \"state\": \"NEW\",\n      \"creationDate\": \"2025-08-02T13:31:48.141053Z\",\n      \"lastUpdateTime\": \"2025-08-02T13:31:48.141053Z\",\n      \"transactionId\": \"<txUUID>\"\n    }\n  }\n}\n```\n\n`payload` is omitted when `attachEntity=false` in the processor config.\n\nClient responds with `EntityProcessorCalculationResponse`:\n\n```json\n{\n  \"requestId\": \"<same requestId>\",\n  \"success\": true,\n  \"payload\": {\n    \"type\": \"JSON\",\n    \"data\": {<optionally updated entity JSON body>}\n  },\n  \"warnings\": [],\n  \"error\": null\n}\n```\n\nWhen `success=false`, the workflow engine fails the processor dispatch. When `payload.data` is non-null, the engine replaces the entity's data with the returned value before continuing the workflow.\n\n**Criteria dispatch (server → client):**\n\nServer sends `EntityCriteriaCalculationRequest` when a workflow transition evaluates a `function`-type criterion:\n\n```json\n{\n  \"id\": \"<requestId>\",\n  \"requestId\": \"<requestId>\",\n  \"entityId\": \"<entityUUID>\",\n  \"criteriaId\": \"my-criteria-fn\",\n  \"criteriaName\": \"my-criteria-fn\",\n  \"target\": \"TRANSITION\",\n  \"workflow\": {\"id\": \"prize-lifecycle\", \"name\": \"prize-lifecycle\"},\n  \"transition\": {\"id\": \"APPROVE\", \"name\": \"APPROVE\"},\n  \"transactionId\": \"<txUUID>\",\n  \"success\": true,\n  \"payload\": { ...same shape as processor payload... }\n}\n```\n\nClient responds with `EntityCriteriaCalculationResponse`:\n\n```json\n{\n  \"requestId\": \"<same requestId>\",\n  \"success\": true,\n  \"matches\": true,\n  \"warnings\": [],\n  \"error\": null\n}\n```\n\n**Auth context on dispatched events:**\n\nThe server attaches CloudEvent Auth Context extension attributes to every dispatched request:\n\n- `authtype` — `\"user\"` or `\"service_account\"` (based on whether the originating user has `ROLE_M2M`)\n- `authid` — the user ID of the originating request\n- `authclaims` — comma-separated roles of the originating user\n\n## KEEPALIVE\n\nThe server sends `CalculationMemberKeepAliveEvent` to each connected member every `CYODA_KEEPALIVE_INTERVAL` seconds. If a member does not respond (via keep-alive echo, processor response, criteria response, or `EventAckResponse`) within `CYODA_KEEPALIVE_TIMEOUT` seconds of the last seen activity, the server closes the stream.\n\n- `CYODA_KEEPALIVE_INTERVAL` — seconds between server-sent keep-alive events (default: `10`)\n- `CYODA_KEEPALIVE_TIMEOUT` — seconds of inactivity before the server terminates the stream (default: `30`)\n\nBoth variables are read by `DefaultConfig()` and applied at gRPC server construction time.\n\n## TAG ROUTING\n\nA compute member declares its tags in `CalculationMemberJoinEvent.tags` as a string slice. The server routes a processor or criteria request to a member whose tags overlap with `calculationNodesTags` (comma-separated) from the processor or criteria config.\n\n`FindByTags` selects the first matching member for the authenticated tenant by iterating over the internal member map. Tag matching uses intersection: the member must declare at least one tag that appears in the processor's `calculationNodesTags`. Because the internal store is a Go map, iteration order is random (non-deterministic per Go specification). When multiple members share a tag, the selected member is chosen at random on each dispatch. Clients requiring deterministic routing must use distinct tags per member.\n\nWhen `calculationNodesTags` is empty, any member for the authenticated tenant matches (still chosen at random when multiple exist).\n\nIn cluster mode, the `ClusterDispatcher` propagates member tag sets across nodes via gossip so any node can forward dispatches to a node that has a matching member.\n\n## ERRORS\n\ngRPC error codes returned by the service:\n\n- `codes.Unauthenticated` — missing or invalid `authorization` metadata\n- `codes.PermissionDenied` — `ROLE_M2M` required for `startStreaming`; tenant mismatch on join\n- `codes.InvalidArgument` — first message is not `CalculationMemberJoinEvent`; malformed CloudEvent; invalid join payload\n- `codes.DeadlineExceeded` — member timed out (keep-alive timeout exceeded)\n- `codes.Internal` — server-side error constructing a response CloudEvent\n\nWithin `text_data` payloads, errors are reported as:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"SERVER_ERROR\",\n    \"message\": \"SERVER_ERROR: internal error [ticket: <uuid>]\",\n    \"retryable\": null\n  }\n}\n```\n\nOperational 4xx errors carry the domain code (e.g. `CLIENT_ERROR`) and a human-readable message. Internal errors use `SERVER_ERROR` with a ticket UUID for server-side log correlation.\n\nProcessor dispatch errors surfaced to the workflow engine:\n\n- `errors.NO_COMPUTE_MEMBER_FOR_TAG` — no member registered for the requested tags\n- `errors.COMPUTE_MEMBER_DISCONNECTED` — member disconnected while a dispatch was in flight (all pending requests receive `\"member disconnected\"` error)\n- `errors.DISPATCH_TIMEOUT` — processor or criteria response not received within `responseTimeoutMs`\n- `errors.DISPATCH_FORWARD_FAILED` — cluster forwarder failed to forward dispatch to remote node\n\n## EXAMPLES\n\n**List services (plaintext, no auth):**\n\n```\ngrpcurl -plaintext localhost:9090 list\n```\n\n**List methods on CloudEventsService:**\n\n```\ngrpcurl -plaintext localhost:9090 list org.cyoda.cloud.api.grpc.CloudEventsService\n```\n\n**Describe the CloudEventsService:**\n\n```\ngrpcurl -plaintext \\\n  -import-path ./proto \\\n  -proto cyoda/cyoda-cloud-api.proto \\\n  localhost:9090 \\\n  describe org.cyoda.cloud.api.grpc.CloudEventsService\n```\n\n**Connect as a compute member (mock auth — no token required):**\n\n```\ngrpcurl -plaintext \\\n  -import-path ./proto \\\n  -proto cyoda/cyoda-cloud-api.proto \\\n  -d '{\"id\":\"join-1\",\"source\":\"client\",\"spec_version\":\"1.0\",\"type\":\"CalculationMemberJoinEvent\",\"text_data\":\"{\\\"id\\\":\\\"join-1\\\",\\\"tags\\\":[\\\"my-service\\\"],\\\"joinedLegalEntityId\\\":\\\"mock-tenant\\\"}\"}' \\\n  localhost:9090 \\\n  org.cyoda.cloud.api.grpc.CloudEventsService/StartStreaming\n```\n\n**Connect as a compute member (JWT auth):**\n\n```\ngrpcurl -plaintext \\\n  -H \"authorization: Bearer $TOKEN\" \\\n  -import-path ./proto \\\n  -proto cyoda/cyoda-cloud-api.proto \\\n  -d '{\"id\":\"join-1\",\"source\":\"client\",\"spec_version\":\"1.0\",\"type\":\"CalculationMemberJoinEvent\",\"text_data\":\"{\\\"id\\\":\\\"join-1\\\",\\\"tags\\\":[\\\"my-service\\\"],\\\"joinedLegalEntityId\\\":\\\"acme-corp\\\"}\"}' \\\n  localhost:9090 \\\n  org.cyoda.cloud.api.grpc.CloudEventsService/StartStreaming\n```\n\n## ACTION DETAILS\n\n- `cyoda help grpc proto` — emit raw `.proto` source for `cyoda-cloud-api.proto` and `cloudevents.proto` (concatenated with separator comments)\n- `cyoda help grpc json` — emit the gRPC service `FileDescriptorSet` as JSON (standard protobuf descriptor form)\n\n## SEE ALSO\n\n- config.grpc\n- workflows\n- errors.COMPUTE_MEMBER_DISCONNECTED\n- errors.NO_COMPUTE_MEMBER_FOR_TAG\n- errors.DISPATCH_TIMEOUT\n- errors.DISPATCH_FORWARD_FAILED\n",
  "sections": [
    {
      "name": "NAME",
      "body": "grpc — gRPC service contract for compute members and entity management."
    },
    {
      "name": "SYNOPSIS",
      "body": "```\ngrpcurl -plaintext localhost:9090 list\ngrpcurl -plaintext localhost:9090 org.cyoda.cloud.api.grpc.CloudEventsService/StartStreaming\n```"
    },
    {
      "name": "DESCRIPTION",
      "body": "cyoda-go exposes one gRPC service: `CloudEventsService` (package `org.cyoda.cloud.api.grpc`). All gRPC methods use the CloudEvents Protobuf envelope (`io.cloudevents.v1.CloudEvent`) as both request and response types. The event type string in the CloudEvent envelope selects the operation; the JSON payload in `text_data` (or `binary_data`) carries the operation-specific body.\n\nThe primary use case is the compute member protocol: external workflow processors subscribe via `StartStreaming`, receive processor and criteria calculation requests, and respond over the same bidirectional stream.\n\nThe secondary use case is programmatic entity and model management: `entityManage`, `entityManageCollection`, `entityModelManage`, `entitySearch`, and `entitySearchCollection` allow gRPC clients to perform the same CRUD operations as the REST API."
    },
    {
      "name": "CONNECTION",
      "body": "**Endpoint**: `host:CYODA_GRPC_PORT` (default `localhost:9090`).\n\n**Transport**: plaintext TCP by default. TLS termination is handled by the ingress or service mesh in production deployments.\n\n**Authentication**: Bearer token passed as gRPC metadata key `authorization`. The value is the same `Bearer <token>` string as used in the HTTP API. Both mock IAM and JWT modes apply identically to gRPC connections — the auth interceptor extracts the `authorization` metadata value, builds an `http.Request` with that `Authorization` header, and delegates to the configured `AuthenticationService`.\n\n**OTel tracing**: when `CYODA_OTEL_ENABLED=true`, the gRPC server installs an `otelgrpc.NewServerHandler()` stats handler that creates spans for every inbound RPC."
    },
    {
      "name": "SERVICES",
      "body": "The proto package is `org.cyoda.cloud.api.grpc`. The Go package is `github.com/cyoda-platform/cyoda-go/api/grpc/cyoda`.\n\n```proto\nsyntax = \"proto3\";\n\npackage org.cyoda.cloud.api.grpc;\n\nimport \"cloudevents/cloudevents.proto\";\n\nservice CloudEventsService {\n  rpc startStreaming(stream io.cloudevents.v1.CloudEvent)\n      returns (stream io.cloudevents.v1.CloudEvent);\n\n  rpc entityModelManage(io.cloudevents.v1.CloudEvent)\n      returns (io.cloudevents.v1.CloudEvent);\n\n  rpc entityManage(io.cloudevents.v1.CloudEvent)\n      returns (io.cloudevents.v1.CloudEvent);\n\n  rpc entityManageCollection(io.cloudevents.v1.CloudEvent)\n      returns (stream io.cloudevents.v1.CloudEvent);\n\n  rpc entitySearch(io.cloudevents.v1.CloudEvent)\n      returns (io.cloudevents.v1.CloudEvent);\n\n  rpc entitySearchCollection(io.cloudevents.v1.CloudEvent)\n      returns (stream io.cloudevents.v1.CloudEvent);\n}\n```\n\n**startStreaming** — bidirectional streaming RPC for compute member lifecycle. Requires `ROLE_M2M`. First message must be `CalculationMemberJoinEvent`. Server sends processor and criteria requests; client sends responses and keep-alive acknowledgments.\n\n**entityModelManage** — unary RPC for entity model operations. Accepts: `EntityModelImportRequest`, `EntityModelExportRequest`, `EntityModelTransitionRequest`, `EntityModelDeleteRequest`, `EntityModelGetAllRequest`.\n\n**entityManage** — unary RPC for single-entity operations. Accepts: `EntityCreateRequest`, `EntityUpdateRequest`, `EntityDeleteRequest`, `EntityDeleteAllRequest`, `EntityTransitionRequest`.\n\n**entityManageCollection** — server-streaming RPC for batch entity operations. Accepts: `EntityCreateCollectionRequest`, `EntityUpdateCollectionRequest`. Streams one response CloudEvent per entity.\n\n**entitySearch** — unary RPC for entity retrieval. Accepts: `EntityGetRequest`, `EntityGetAllRequest`, `EntitySnapshotSearchRequest`, `EntitySearchRequest`, `SnapshotCancelRequest`, `SnapshotGetRequest`, `SnapshotGetStatusRequest`, `EntityStatsGetRequest`, `EntityStatsByStateGetRequest`, `EntityChangesMetadataGetRequest`.\n\n**entitySearchCollection** — server-streaming RPC for collection retrieval. Streams results."
    },
    {
      "name": "MESSAGE TYPES",
      "body": "All CloudEvents are encoded in the Protobuf CloudEvent format. The `type` field selects the operation. The `text_data` field carries the JSON-encoded payload.\n\n**CloudEvent envelope** (`io.cloudevents.v1.CloudEvent`):\n\n```proto\nmessage CloudEvent {\n  string id = 1;          // UUID\n  string source = 2;      // \"cyoda\"\n  string spec_version = 3; // \"1.0\"\n  string type = 4;         // event type constant\n\n  map<string, CloudEventAttributeValue> attributes = 5;\n\n  oneof data {\n    bytes  binary_data = 6;\n    string text_data   = 7;   // JSON payload\n    google.protobuf.Any proto_data = 8;\n  }\n}\n```\n\n**Streaming event types** (compute member protocol):\n\n- `CalculationMemberJoinEvent` — first message from client; registers the member\n- `CalculationMemberGreetEvent` — server response to join; includes assigned member ID\n- `CalculationMemberKeepAliveEvent` — bidirectional; server sends on interval, client echoes\n- `EntityProcessorCalculationRequest` — server → client; processor dispatch request\n- `EntityProcessorCalculationResponse` — client → server; processor result\n- `EntityCriteriaCalculationRequest` — server → client; criteria dispatch request\n- `EntityCriteriaCalculationResponse` — client → server; criteria result\n- `EventAckResponse` — client → server; acknowledges any server event\n\n**EventAckResponse `text_data` JSON shape:**\n\n```json\n{\n  \"id\": \"<uuid for this ack message>\",\n  \"sourceEventId\": \"<id of the server event being acknowledged>\",\n  \"success\": true,\n  \"warnings\": [],\n  \"error\": null\n}\n```\n\nFields:\n- `id` (string, required) — unique identifier for this ack message; any UUID\n- `sourceEventId` (string, required) — the `id` field from the server CloudEvent being acknowledged\n- `success` (boolean, optional, default `true`) — set to `true` for a normal ack; `false` if the client is reporting a processing error\n- `warnings` (string array, optional) — diagnostic messages; may be omitted\n- `error` (object, optional) — present only when `success=false`; shape: `{\"code\":\"<code>\",\"message\":\"<msg>\",\"retryable\":<bool|null>}`\n\nThe full CloudEvent envelope for an ack:\n\n```json\n{\n  \"id\": \"<ack-uuid>\",\n  \"source\": \"client\",\n  \"spec_version\": \"1.0\",\n  \"type\": \"EventAckResponse\",\n  \"text_data\": \"{\\\"id\\\":\\\"<ack-uuid>\\\",\\\"sourceEventId\\\":\\\"<server-event-id>\\\",\\\"success\\\":true}\"\n}\n```\n\n`EventAckResponse` updates the member's last-seen timestamp, preventing keep-alive timeout. It is used to acknowledge any server event for which the client has no substantive response (e.g. a keep-alive or a greet event).\n\n**Entity management event types**:\n\n- `EntityCreateRequest` / `EntityTransactionResponse`\n- `EntityCreateCollectionRequest` / `EntityTransactionResponse` (streamed)\n- `EntityUpdateRequest` / `EntityTransactionResponse`\n- `EntityUpdateCollectionRequest` / `EntityTransactionResponse` (streamed)\n- `EntityDeleteRequest` / `EntityDeleteResponse`\n- `EntityDeleteAllRequest` / `EntityDeleteAllResponse`\n- `EntityTransitionRequest` / `EntityTransitionResponse`\n\n**Model management event types**:\n\n- `EntityModelImportRequest` / `EntityModelImportResponse`\n- `EntityModelExportRequest` / `EntityModelExportResponse`\n- `EntityModelTransitionRequest` / `EntityModelTransitionResponse`\n- `EntityModelDeleteRequest` / `EntityModelDeleteResponse`\n- `EntityModelGetAllRequest` / `EntityModelGetAllResponse`\n\n**Search / query event types**:\n\n- `EntityGetRequest` / `EntityResponse`\n- `EntityGetAllRequest` / `EntityResponse` (streamed via entitySearchCollection)\n- `EntitySnapshotSearchRequest` / `EntitySnapshotSearchResponse`\n- `EntitySearchRequest` / `EntityResponse`\n- `SnapshotCancelRequest` / `EntitySnapshotSearchResponse`\n- `SnapshotGetRequest` / `EntitySnapshotSearchResponse`\n- `SnapshotGetStatusRequest` / `EntitySnapshotSearchResponse`\n- `EntityStatsGetRequest` / `EntityStatsResponse`\n- `EntityStatsByStateGetRequest` / `EntityStatsByStateResponse`\n- `EntityChangesMetadataGetRequest` / `EntityChangesMetadataResponse`"
    },
    {
      "name": "COMPUTE MEMBER PROTOCOL",
      "body": "The compute member protocol allows external processes to serve as workflow processor and criteria nodes.\n\n**Join sequence:**\n\n1. Client opens `startStreaming` with `Authorization: Bearer <token>` metadata. Token must carry `ROLE_M2M`.\n2. Client sends `CalculationMemberJoinEvent` as the first message:\n\n```json\n{\n  \"id\": \"<uuid>\",\n  \"tags\": [\"approval-service\", \"notification\"],\n  \"joinedLegalEntityId\": \"acme-corp\"\n}\n```\n\n`joinedLegalEntityId` must match the tenant ID in the bearer token. When present and mismatched, the server returns `codes.PermissionDenied`. When absent, the server uses the token's tenant ID implicitly. Include `joinedLegalEntityId` in all join messages — clients that omit it against a strict server may fail if validation is tightened.\n\n3. Server registers the member and responds with `CalculationMemberGreetEvent`:\n\n```json\n{\n  \"id\": \"<server-assigned-member-uuid>\",\n  \"memberId\": \"<server-assigned-member-uuid>\",\n  \"joinedLegalEntityId\": \"<tenantId>\",\n  \"success\": true\n}\n```\n\n**Processor dispatch (server → client):**\n\nServer sends `EntityProcessorCalculationRequest` when a workflow transition invokes an `EXTERNAL` processor whose `calculationNodesTags` matches one of the member's declared tags:\n\n```json\n{\n  \"id\": \"<requestId>\",\n  \"requestId\": \"<requestId>\",\n  \"entityId\": \"<entityUUID>\",\n  \"processorId\": \"notify-approval\",\n  \"processorName\": \"notify-approval\",\n  \"workflow\": {\"id\": \"prize-lifecycle\", \"name\": \"prize-lifecycle\"},\n  \"transition\": {\"id\": \"APPROVE\", \"name\": \"APPROVE\"},\n  \"transactionId\": \"<txUUID>\",\n  \"success\": true,\n  \"payload\": {\n    \"type\": \"JSON\",\n    \"data\": {<entity JSON body>},\n    \"meta\": {\n      \"id\": \"<entityUUID>\",\n      \"modelKey\": {\"name\": \"nobel-prize\", \"version\": 1},\n      \"state\": \"NEW\",\n      \"creationDate\": \"2025-08-02T13:31:48.141053Z\",\n      \"lastUpdateTime\": \"2025-08-02T13:31:48.141053Z\",\n      \"transactionId\": \"<txUUID>\"\n    }\n  }\n}\n```\n\n`payload` is omitted when `attachEntity=false` in the processor config.\n\nClient responds with `EntityProcessorCalculationResponse`:\n\n```json\n{\n  \"requestId\": \"<same requestId>\",\n  \"success\": true,\n  \"payload\": {\n    \"type\": \"JSON\",\n    \"data\": {<optionally updated entity JSON body>}\n  },\n  \"warnings\": [],\n  \"error\": null\n}\n```\n\nWhen `success=false`, the workflow engine fails the processor dispatch. When `payload.data` is non-null, the engine replaces the entity's data with the returned value before continuing the workflow.\n\n**Criteria dispatch (server → client):**\n\nServer sends `EntityCriteriaCalculationRequest` when a workflow transition evaluates a `function`-type criterion:\n\n```json\n{\n  \"id\": \"<requestId>\",\n  \"requestId\": \"<requestId>\",\n  \"entityId\": \"<entityUUID>\",\n  \"criteriaId\": \"my-criteria-fn\",\n  \"criteriaName\": \"my-criteria-fn\",\n  \"target\": \"TRANSITION\",\n  \"workflow\": {\"id\": \"prize-lifecycle\", \"name\": \"prize-lifecycle\"},\n  \"transition\": {\"id\": \"APPROVE\", \"name\": \"APPROVE\"},\n  \"transactionId\": \"<txUUID>\",\n  \"success\": true,\n  \"payload\": { ...same shape as processor payload... }\n}\n```\n\nClient responds with `EntityCriteriaCalculationResponse`:\n\n```json\n{\n  \"requestId\": \"<same requestId>\",\n  \"success\": true,\n  \"matches\": true,\n  \"warnings\": [],\n  \"error\": null\n}\n```\n\n**Auth context on dispatched events:**\n\nThe server attaches CloudEvent Auth Context extension attributes to every dispatched request:\n\n- `authtype` — `\"user\"` or `\"service_account\"` (based on whether the originating user has `ROLE_M2M`)\n- `authid` — the user ID of the originating request\n- `authclaims` — comma-separated roles of the originating user"
    },
    {
      "name": "KEEPALIVE",
      "body": "The server sends `CalculationMemberKeepAliveEvent` to each connected member every `CYODA_KEEPALIVE_INTERVAL` seconds. If a member does not respond (via keep-alive echo, processor response, criteria response, or `EventAckResponse`) within `CYODA_KEEPALIVE_TIMEOUT` seconds of the last seen activity, the server closes the stream.\n\n- `CYODA_KEEPALIVE_INTERVAL` — seconds between server-sent keep-alive events (default: `10`)\n- `CYODA_KEEPALIVE_TIMEOUT` — seconds of inactivity before the server terminates the stream (default: `30`)\n\nBoth variables are read by `DefaultConfig()` and applied at gRPC server construction time."
    },
    {
      "name": "TAG ROUTING",
      "body": "A compute member declares its tags in `CalculationMemberJoinEvent.tags` as a string slice. The server routes a processor or criteria request to a member whose tags overlap with `calculationNodesTags` (comma-separated) from the processor or criteria config.\n\n`FindByTags` selects the first matching member for the authenticated tenant by iterating over the internal member map. Tag matching uses intersection: the member must declare at least one tag that appears in the processor's `calculationNodesTags`. Because the internal store is a Go map, iteration order is random (non-deterministic per Go specification). When multiple members share a tag, the selected member is chosen at random on each dispatch. Clients requiring deterministic routing must use distinct tags per member.\n\nWhen `calculationNodesTags` is empty, any member for the authenticated tenant matches (still chosen at random when multiple exist).\n\nIn cluster mode, the `ClusterDispatcher` propagates member tag sets across nodes via gossip so any node can forward dispatches to a node that has a matching member."
    },
    {
      "name": "ERRORS",
      "body": "gRPC error codes returned by the service:\n\n- `codes.Unauthenticated` — missing or invalid `authorization` metadata\n- `codes.PermissionDenied` — `ROLE_M2M` required for `startStreaming`; tenant mismatch on join\n- `codes.InvalidArgument` — first message is not `CalculationMemberJoinEvent`; malformed CloudEvent; invalid join payload\n- `codes.DeadlineExceeded` — member timed out (keep-alive timeout exceeded)\n- `codes.Internal` — server-side error constructing a response CloudEvent\n\nWithin `text_data` payloads, errors are reported as:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"SERVER_ERROR\",\n    \"message\": \"SERVER_ERROR: internal error [ticket: <uuid>]\",\n    \"retryable\": null\n  }\n}\n```\n\nOperational 4xx errors carry the domain code (e.g. `CLIENT_ERROR`) and a human-readable message. Internal errors use `SERVER_ERROR` with a ticket UUID for server-side log correlation.\n\nProcessor dispatch errors surfaced to the workflow engine:\n\n- `errors.NO_COMPUTE_MEMBER_FOR_TAG` — no member registered for the requested tags\n- `errors.COMPUTE_MEMBER_DISCONNECTED` — member disconnected while a dispatch was in flight (all pending requests receive `\"member disconnected\"` error)\n- `errors.DISPATCH_TIMEOUT` — processor or criteria response not received within `responseTimeoutMs`\n- `errors.DISPATCH_FORWARD_FAILED` — cluster forwarder failed to forward dispatch to remote node"
    },
    {
      "name": "EXAMPLES",
      "body": "**List services (plaintext, no auth):**\n\n```\ngrpcurl -plaintext localhost:9090 list\n```\n\n**List methods on CloudEventsService:**\n\n```\ngrpcurl -plaintext localhost:9090 list org.cyoda.cloud.api.grpc.CloudEventsService\n```\n\n**Describe the CloudEventsService:**\n\n```\ngrpcurl -plaintext \\\n  -import-path ./proto \\\n  -proto cyoda/cyoda-cloud-api.proto \\\n  localhost:9090 \\\n  describe org.cyoda.cloud.api.grpc.CloudEventsService\n```\n\n**Connect as a compute member (mock auth — no token required):**\n\n```\ngrpcurl -plaintext \\\n  -import-path ./proto \\\n  -proto cyoda/cyoda-cloud-api.proto \\\n  -d '{\"id\":\"join-1\",\"source\":\"client\",\"spec_version\":\"1.0\",\"type\":\"CalculationMemberJoinEvent\",\"text_data\":\"{\\\"id\\\":\\\"join-1\\\",\\\"tags\\\":[\\\"my-service\\\"],\\\"joinedLegalEntityId\\\":\\\"mock-tenant\\\"}\"}' \\\n  localhost:9090 \\\n  org.cyoda.cloud.api.grpc.CloudEventsService/StartStreaming\n```\n\n**Connect as a compute member (JWT auth):**\n\n```\ngrpcurl -plaintext \\\n  -H \"authorization: Bearer $TOKEN\" \\\n  -import-path ./proto \\\n  -proto cyoda/cyoda-cloud-api.proto \\\n  -d '{\"id\":\"join-1\",\"source\":\"client\",\"spec_version\":\"1.0\",\"type\":\"CalculationMemberJoinEvent\",\"text_data\":\"{\\\"id\\\":\\\"join-1\\\",\\\"tags\\\":[\\\"my-service\\\"],\\\"joinedLegalEntityId\\\":\\\"acme-corp\\\"}\"}' \\\n  localhost:9090 \\\n  org.cyoda.cloud.api.grpc.CloudEventsService/StartStreaming\n```"
    },
    {
      "name": "ACTION DETAILS",
      "body": "- `cyoda help grpc proto` — emit raw `.proto` source for `cyoda-cloud-api.proto` and `cloudevents.proto` (concatenated with separator comments)\n- `cyoda help grpc json` — emit the gRPC service `FileDescriptorSet` as JSON (standard protobuf descriptor form)"
    },
    {
      "name": "SEE ALSO",
      "body": "- config.grpc\n- workflows\n- errors.COMPUTE_MEMBER_DISCONNECTED\n- errors.NO_COMPUTE_MEMBER_FOR_TAG\n- errors.DISPATCH_TIMEOUT\n- errors.DISPATCH_FORWARD_FAILED"
    }
  ],
  "see_also": [
    "config.grpc",
    "workflows",
    "errors.COMPUTE_MEMBER_DISCONNECTED",
    "errors.NO_COMPUTE_MEMBER_FOR_TAG",
    "errors.DISPATCH_TIMEOUT",
    "errors.DISPATCH_FORWARD_FAILED"
  ],
  "stability": "stable",
  "actions": [
    "json",
    "proto"
  ]
}
