Workflow service internals — the stmcmds conventions

A Chenile workflow service (generated by the wfservice / wfcustom blueprints) runs a Finito state machine, but the engine itself is generic. Application behaviour is discovered by naming convention rather than wired per transition. The glue lives in workflow-service/…/workflow/service/stmcmds (repository chenile-query-workflow-blueprints). Knowing these conventions is what lets you add business logic by dropping in a correctly named Spring bean.

One event, end to end

Take PATCH /issue/{id}/assign:

  1. The workflow controller’s processById(id, eventID, payload) receives event assign.
  2. StmBodyTypeSelector decides what type the JSON payload should become (see below).
  3. The STM starts the transition.
  4. BaseTransitionAction runs as the default transition action.
  5. With no metadata-driven OWIZ command, it asks STMTransitionActionResolver for the bean for assign → issueAssignAction.
  6. That action — usually an AbstractSTMTransitionAction<Issue, AssignIssuePayload> — runs typed business logic.
  7. Any secondary actions registered for assign run afterwards, in order.
  8. GenericEntryAction stamps state-entry time and SLA fields, stores the entity via EntityStore, and calls the state’s post-save hook if one exists.
  9. The response returns mutatedEntity plus allowedActionsAndMetadata for the new state.

The naming convention

STMTransitionActionResolver builds bean names from a workflow prefix, the event or state id, and (in suffix mode) a type suffix:

Component Suffix mode (useSuffix = true) Without suffix
Transition action for event assign issueAssignAction issueAssign
Post-save hook for state resolved issueResolvedPostSaveHook issueResolved
Auto-state for state readyForClosure issueReadyForClosureAutoState issueReadyForClosure

The resolver can also use context-sensitive prefixes from headers in ContextContainer, so a tenant- or client-specific bean overrides the default — the same mechanism trajectories use. Because the names are the contract, renaming an event means renaming its bean.

The classes, and what each is for

Class Role
BaseTransitionAction The default dispatcher. In order: run a direct OWIZ command bean named in transition metadata → an orchExecutor bean → an orchestration XML (orchestratedCommandsConfiguration) → otherwise the convention-resolved action. Also logs activities and enforces mandatory activities when an ActivityChecker is present.
AbstractSTMTransitionAction Base for event actions. Declares a strongly typed payload as the second parameter of transitionTo(...) — which is also how payload types are inferred.
StmBodyTypeSelector Picks the event payload type: an explicit bodyType in event metadata, else the reflected second parameter of the resolved action’s transitionTo(...). One controller method serves every event with its own payload class.
GenericEntryAction Persistence boundary: entry time, SLA timing (late / tending-late), EntityStore save, then PostSaveHook.
GenericExitAction A placeholder extension point for exit behaviour.
DefaultPostSaveHook Resolves a state-specific PostSaveHook by convention and delegates — the safe place for notifications, integrations and audit.
DefaultAutomaticStateComputation Resolves a state-specific auto-state bean by convention — decisions that pick their own transition.
MultipleCommandsRegistry + SecondSTMTransitionAction Follow-up actions for the same event: a SecondSTMTransitionAction registers itself for one or more event ids with an order index at startup and runs after the primary action.
GenericRetrievalStrategy Loads the persisted entity from EntityStore when the incoming object only carries an id.
StmAuthoritiesBuilder Maps an incoming event to the authorities it requires, from meta-acls on the transition; with default ACLs enabled it derives a fallback authority from the service and event id. Generated controllers use it as @SecurityConfig(authoritiesSupplier = "<svc>EventAuthoritiesSupplier").
ProcessIdPolymorph Publishes each event’s payload variant as polymorphic metadata for MCP agents. Lives in the optional workflow-mcp module so the base runtime doesn’t depend on chenile-mcp.

What clients see

Every operation returns StateEntityServiceResponse { mutatedEntity, allowedActionsAndMetadata }. Each allowed action is {"allowedAction": "<eventId>", …} plus every meta-* attribute declared on that transition in the states XML (prefix stripped — meta-acls → "acls", meta-activity → "activity"), already filtered by the current state and security strategy. Build UI buttons from it rather than hard-coding events. Workflow-backed queries (workflowName in the query metadata) return the same per-row allowedActions.

Introspection endpoints under /{svc}/info/ — state-diagram, allowed-actions?state=, json, test-cases, test-visualization, test-state-diagrams — expose the flow itself; the Admin UI renders them.

Design trade-off

Convention over explicit wiring keeps application code small and generator-friendly — that is why JGen can emit small, mostly declarative workflow services. The cost is that names become part of the contract: when you change StmBodyTypeSelector, check every getConfigs() consumer; when you change workflow wiring, check the runtime classes and the JGen bp-wfservice / bp-wfcustom templates.

Where to look
workflow-service/src/main/java/org/chenile/workflow/service/stmcmds and workflow-api/…/StateEntityService.java in ajapros/chenile-query-workflow-blueprints; long-form guide docs/WORKFLOW_STMCMDS_GUIDE.md. Compile check: mvn -pl workflow-service -am -DskipTests compile.