workflows — state machine definitions
cyoda-go version 0.7.1
workflows
Section titled “workflows”workflows — workflow state machine definitions: states, transitions, processors, and criteria.
SYNOPSIS
Section titled “SYNOPSIS”POST /api/model/{entityName}/{modelVersion}/workflow/importGET /api/model/{entityName}/{modelVersion}/workflow/exportContext path prefix is CYODA_CONTEXT_PATH (default /api). All endpoints require Authorization: Bearer <token> except when CYODA_IAM_MODE=mock.
DESCRIPTION
Section titled “DESCRIPTION”A workflow definition is a named finite state machine attached to an entity model. Workflows are stored per model reference (entityName, modelVersion). A model may have multiple workflow definitions; the engine selects the matching one per entity using the workflow-level criterion field evaluated at entity creation time. When no criterion matches, the engine uses the default built-in workflow.
The engine executes automatically after every entity write. It sets the initial state, evaluates automated transitions (cascade), and invokes processors on each transition. Manual transitions are triggered by the client via PUT /entity/{format}/{entityId}/{transition}.
The engine enforces a per-state visit limit of 10 by default (configurable via WithMaxStateVisits) and an absolute cascade depth limit of 100 to prevent infinite loops. Static cycle detection runs at import time.
WORKFLOW SCHEMA
Section titled “WORKFLOW SCHEMA”WorkflowDefinition (element of the workflows array in import):
{ "version": "1", "name": "prize-lifecycle", "desc": "State machine for Nobel Prize entities", "initialState": "NEW", "active": true, "criterion": null, "states": { "NEW": { "transitions": [ { "name": "APPROVE", "next": "APPROVED", "manual": true, "disabled": false, "criterion": null, "processors": [ { "type": "EXTERNAL", "name": "notify-approval", "executionMode": "SYNC", "config": { "attachEntity": true, "calculationNodesTags": "approval-service", "responseTimeoutMs": 30000, "retryPolicy": "", "context": "" } } ] }, { "name": "AUTO_VALIDATE", "next": "VALIDATED", "manual": false, "disabled": false, "criterion": { "type": "simple", "jsonPath": "$.year", "operatorType": "EQUALS", "value": "2024" }, "processors": [] } ] }, "APPROVED": { "transitions": [] }, "VALIDATED": { "transitions": [] } }}WorkflowDefinition fields:
version— string — schema version tag (informational; not interpreted by the engine)name— string — unique within the model; the primary key for MERGE modedesc— string — optional descriptioninitialState— string — state assigned when the entity is first created; must exist instatesactive— boolean — whenfalse, the engine skips this workflow during selectioncriterion—ConditionJSON ornull— evaluated against the entity at creation to select this workflow;nullmatches all entitiesstates— object — map of state name →StateDefinition
StateDefinition:
transitions— array ofTransitionDefinition— may be empty
TRANSITIONS
Section titled “TRANSITIONS”TransitionDefinition fields:
name— string — transition name; used by the client inPUT /entity/{format}/{entityId}/{name}and in engine cascadenext— string — target state; must exist instatesmanual— boolean —truemeans the transition requires an explicit client request;falsemeans the engine evaluates it automatically in cascadedisabled— boolean — whentrue, the engine skips this transition entirelycriterion—ConditionJSON ornull— evaluated before executing the transition;nullmeans always matches; the same Condition DSL as search (seesearchtopic)processors— array ofProcessorDefinition— invoked sequentially on this transition
PROCESSORS
Section titled “PROCESSORS”ProcessorDefinition fields:
type— string — processor type; see valid values belowname— string — logical processor nameexecutionMode— string — execution mode; see valid values belowconfig—ProcessorConfig
Valid type values (exhaustive for v0.6.1):
"EXTERNAL"— dispatches to a calculation node via gRPC usingcalculationNodesTagsfor routing
No other types are supported. Supplying any other value produces errors.VALIDATION_FAILED at workflow import time.
Valid executionMode values (exhaustive):
"SYNC"— the engine dispatches the processor and blocks until a response is received; the entity write transaction remains open during the wait; processor failure (including timeout andsuccess=falsein the response) returnserrors.WORKFLOW_FAILED(400) and the entity remains in the source state"ASYNC_SAME_TX"— same dispatch mechanics asSYNC(blocks inline, transaction stays open); failure semantics are identical toSYNC"ASYNC_NEW_TX"— dispatched within a savepoint; on failure the savepoint is rolled back and the error is logged as a warning; the pipeline continues to the next processor and the transition completes; returned entity modifications are discarded"COMMIT_BEFORE_DISPATCH"— the engine splits the cascade into two transactions around this processor.TX_preflushes the pre-callout state of the transition and commits before the processor is dispatched, releasing the storage connection for the duration of the external compute. The processor runs outside any transaction (entity already durable in the pre-callout state). When the processor returns, the engine opensTX_poston the same node, reapplies the result viaCompareAndSave(CAS expects the txID stamped atTX_pre’s commit), runs any subsequent SYNC processors and cascade transitions, then commits. CAS conflict at the boundary surfaces as409 retryable; entity remains durable in the pre-callout state, no engine-side retry, no automatic compensation. Failure of the dispatched processor (success=false, timeout, member crash) returnserrors.WORKFLOW_FAILED(400) and the entity remains in the pre-callout state. Designed to relieve connection-pool pressure for slow processors and supersedesASYNC_NEW_TXas the recommended mode for slow external work.
COMMIT_BEFORE_DISPATCH configuration flag:
startNewTxOnDispatch— boolean — sibling field on the same processor object; defaultfalse; valid only whenexecutionMode == "COMMIT_BEFORE_DISPATCH". Validator rejectstruefor any other mode. Whentrue, the engine opens a fresh transaction context (TX_post) for the dispatched processor’s CRUD callbacks; the processor may use the supplied transaction token to read or write entities other than the cascade-anchor. Whenfalse, no transaction context is supplied to the dispatched call.
COMMIT_BEFORE_DISPATCH workflow-author requirements:
- Idempotency. A
COMMIT_BEFORE_DISPATCHprocessor must be idempotent or have an external mechanism for detecting prior completion (e.g., a write-once external resource ID). Replays can fire from two distinct places: (a) CAS conflict during continuation — the caller’s retry of the same API call restarts the cascade and re-dispatches the processor; (b) engine crash between segments — the entity is durable in the pre-callout state, the in-flight orchestration is gone, the caller retries, the cascade re-fires from the beginning, the processor is re-dispatched. The engine cannot deduplicate replays; idempotency is the workflow author’s responsibility. - Visibility of segment-boundary states. States on a segment boundary (the pre-callout state of a
COMMIT_BEFORE_DISPATCHprocessor) are publicly observable to readers between segments. A concurrent transaction’sGet/GetAll/Search/Countwill see the entity in the pre-callout state, and a second cascade may decide to fire criteria-driven transitions based on that observed state. Workflow authors usingCOMMIT_BEFORE_DISPATCHmust treat segment-boundary states as committed states — design state-machine criteria, transition guards, and external monitoring accordingly. If invisibility of an intermediate state is required, model it as a workflow-levelDRAFTparent state with sub-stages in payload, or do not expose the entity until a designated terminal state. - Best-practice: a processor must not save the entity it is processing for. Processors with TX-callback access (SYNC, ASYNC_SAME_TX, COMMIT_BEFORE_DISPATCH with startNewTxOnDispatch=true) can write the cascade-anchor entity via the supplied transaction token, but if they do AND also return mutations for the same entity in their result, the engine’s apply-result will overwrite the processor’s intra-TX writes (last-writer-wins inside the transaction buffer). Pick one path: let the engine apply the result, OR have the processor write the entity itself and return no mutations for it.
An invalid executionMode value is treated as SYNC / ASYNC_SAME_TX (the engine’s default branch). It is not rejected at import time but produces undefined behaviour and must not be relied upon.
ProcessorConfig fields:
attachEntity— boolean — whentrue, the full entity payload is sent to the processorcalculationNodesTags— string — comma-separated tags for routing to registered calculation nodes; the engine selects a node that declares all required tags; returnserrors.NO_COMPUTE_MEMBER_FOR_TAGif no node matchesresponseTimeoutMs— int64 — timeout in milliseconds forSYNCprocessor response;0means use node defaultretryPolicy— string — retry policy name (plugin/platform-defined); empty means no retrycontext— string — arbitrary string forwarded to the processor as context metadata
CRITERIA
Section titled “CRITERIA”Criteria on workflows and transitions use the same Condition DSL as search. All four condition types are supported: simple, lifecycle, group, array. Criteria are evaluated in-memory against the entity’s JSON payload and lifecycle metadata.
simple criteria match entity data fields via JSONPath. lifecycle criteria match state, creationDate, or previousTransition from entity metadata.
A null criterion on a workflow means the workflow matches any entity. A null criterion on a transition means the transition always fires (automated) or is always available (manual). When multiple automated transitions are eligible, the engine selects the first one by declaration order whose criterion matches. A null criterion matches unconditionally, so a null-criterion automated transition must be the last automated transition in declaration order; any automated transitions declared after a null-criterion transition are unreachable.
IMPORT REQUEST
Section titled “IMPORT REQUEST”POST /api/model/{entityName}/{modelVersion}/workflow/import
entityName(path): stringmodelVersion(path): int32
Request body (application/json):
{ "importMode": "MERGE", "workflows": [ { ...WorkflowDefinition... } ]}importMode—"MERGE"(default): incoming workflows overwrite existing ones by name; existing workflows not in the import are preserved."REPLACE": all existing workflows are discarded; only the incoming set is stored."ACTIVATE": incoming workflows replace same-named existing ones and are setactive=true; existing workflows not in the import set are setactive=false.workflows— array ofWorkflowDefinition; all imported workflows are setactive=trueregardless of theactivefield in the body
Static validation runs before saving: definite infinite loops (cycles reachable only via automated transitions) cause 400 VALIDATION_FAILED.
Response: 200 OK, application/json:
{"success": true}EXPORT RESPONSE
Section titled “EXPORT RESPONSE”GET /api/model/{entityName}/{modelVersion}/workflow/export
Response: 200 OK, application/json:
{ "entityName": "nobel-prize", "modelVersion": 1, "workflows": [ { ...WorkflowDefinition... } ]}Returns 404 WORKFLOW_NOT_FOUND when no workflows have been imported for the model.
Export field omission: The export response omits optional fields that were not explicitly set or are default values. Specifically, TransitionDefinition objects in the export may omit disabled (when false) and processors (when empty). States with no transitions are serialised as {} rather than {"transitions":[]}. The desc field on WorkflowDefinition is omitted when empty.
ENGINE EXECUTION
Section titled “ENGINE EXECUTION”The workflow engine runs synchronously within the entity write transaction. The execution sequence for a CREATE:
- Load workflow definitions for the model.
- Evaluate each workflow’s
criterionagainst the entity; select the first match. If none match, use the built-in default workflow. - Set
entity.Meta.State = workflow.initialState. - If a named transition was requested (by the client), execute it: evaluate
criterion, invoke processors, setentity.Meta.State = transition.next. - Cascade: repeatedly scan the current state’s transitions; for each automated (
manual=false) non-disabled transition, evaluatecriterion; if it matches, invoke processors and advance the state. Stop when no automated transition matches or the state has no automated transitions. - The engine records
StateMachineEvententries to the audit log under the entity’stransactionId.
Per-state visit limit (default 10) and total cascade depth limit (100) are enforced to prevent infinite loops.
ERRORS
Section titled “ERRORS”errors.TRANSITION_NOT_FOUND—404— named transition does not exist in the current state’s workflowerrors.WORKFLOW_NOT_FOUND—404— no workflows found for the model (export endpoint)errors.WORKFLOW_FAILED— workflow engine encountered an unrecoverable error during executionerrors.NO_COMPUTE_MEMBER_FOR_TAG— no registered calculation node matches the requiredcalculationNodesTagserrors.COMPUTE_MEMBER_DISCONNECTED— a calculation node disconnected during processor dispatcherrors.VALIDATION_FAILED—400— static cycle detection failed during workflow import
EXAMPLES
Section titled “EXAMPLES”Import a workflow:
curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "importMode": "MERGE", "workflows": [ { "version": "1", "name": "prize-lifecycle", "initialState": "NEW", "active": true, "states": { "NEW": { "transitions": [ { "name": "APPROVE", "next": "APPROVED", "manual": true, "processors": [] } ] }, "APPROVED": { "transitions": [] } } } ] }' \ "http://localhost:8080/api/model/nobel-prize/1/workflow/import"Export workflows:
curl -s -H "Authorization: Bearer $TOKEN" \ "http://localhost:8080/api/model/nobel-prize/1/workflow/export"Trigger a manual transition:
curl -s -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"category":"physics","year":"2024"}' \ "http://localhost:8080/api/entity/JSON/74807f00-ed0d-11ee-a357-ae468cd3ed16/APPROVE"Replace all workflows:
curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "importMode": "REPLACE", "workflows": [ { "version": "1", "name": "simple-wf", "initialState": "OPEN", "active": true, "states": { "OPEN": { "transitions": [] } } } ] }' \ "http://localhost:8080/api/model/nobel-prize/1/workflow/import"SEE ALSO
Section titled “SEE ALSO”- models
- crud
- grpc
- search
- errors.TRANSITION_NOT_FOUND
- errors.WORKFLOW_NOT_FOUND
- errors.WORKFLOW_FAILED
- errors.NO_COMPUTE_MEMBER_FOR_TAG
- errors.COMPUTE_MEMBER_DISCONNECTED
See also
Section titled “See also”cyoda help models— A model is a named, versioned schema registered per tenant. Every entity in the system is an instance of exactly one model. Models are identified by(entityName, modelVersion). The model ID is a deterministic UUID v5 derived from that key:UUID.newSHA1(NameSpaceURL, "{entityName}.{modelVersion}").cyoda help crud— Entities are instances of models. Each entity has a UUID, a model reference (entityName,modelVersion), and a lifecycle state managed by the workflow engine. Creating an entity requires the referenced model to be inLOCKEDstate. All write operations run within a Cyoda transaction and return atransactionIdalongside the affected entity IDs.cyoda help grpc— cyoda-go exposes one gRPC service:CloudEventsService(packageorg.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 intext_data(orbinary_data) carries the operation-specific body.cyoda help search— Search operates against a specific entity model(entityName, modelVersion). Two modes are supported:cyoda help errors TRANSITION_NOT_FOUND— Entity workflow state machines define explicit transitions between states. This error fires when a transition is triggered that does not exist in the model’s workflow definition for the entity’s current state. Also occurs when the transition name is misspelled or when the entity is in a terminal state that allows no further transitions.cyoda help errors WORKFLOW_NOT_FOUND— Entity models reference a workflow by name to govern state transitions. This error is returned when the named workflow cannot be found in the tenant’s workflow registry, during entity type registration or when a model references a workflow that was deleted.cyoda help errors WORKFLOW_FAILED— During an entity create or transition operation the associated workflow processors (pre-processors, post-processors) or guard conditions ran but one of them signalled failure. The failure message from the processor is included in the error detail.cyoda help errors NO_COMPUTE_MEMBER_FOR_TAG— Workflow processors are dispatched to nodes that advertise matching compute tags. When no node with the required tag is alive in the cluster within the configured wait timeout (CYODA_DISPATCH_WAIT_TIMEOUT), the operation is rejected with this error.cyoda help errors COMPUTE_MEMBER_DISCONNECTED— The compute member responsible for executing a processor or workflow step disconnected before completing the operation. The task may or may not have been executed.
Raw formats
Section titled “Raw formats”/help/workflows.json— full descriptor (matchesGET /help/{topic}envelope)/help/workflows.md— body only