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:
- The workflow controller’s
processById(id, eventID, payload)receives eventassign. StmBodyTypeSelectordecides what type the JSON payload should become (see below).- The STM starts the transition.
BaseTransitionActionruns as the default transition action.- With no metadata-driven OWIZ command, it asks
STMTransitionActionResolverfor the bean forassign→issueAssignAction. - That action — usually an
AbstractSTMTransitionAction<Issue, AssignIssuePayload>— runs typed business logic. - Any secondary actions registered for
assignrun afterwards, in order. GenericEntryActionstamps state-entry time and SLA fields, stores the entity viaEntityStore, and calls the state’s post-save hook if one exists.- The response returns
mutatedEntityplusallowedActionsAndMetadatafor 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.
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.