Finito Finite state machines for Java

State machines that run, wire & draw themselves.

Your workflow mentor — from start to finish

Finito models a workflow as states and the events that move between them. Define it in XML, a fluent Java API, or JSON; add actions, hooks and persistence by convention; and get a Mermaid & PlantUML diagram for free.

Part of the Chenile group Runs standalone — plain Java, no framework required org.chenile.stm
define → STM.proceed(entity, event) → actions · auto-persist · post-save hooks · diagrams
The idea

A workflow is a set of states and the events that move between them

Your entity carries its own state; one call — STM.proceed(entity, event, payload) — advances it, running every action, guard and hook along the way.

🟢

States

Manual states wait for an event. Auto states decide their own next move by evaluating a condition on the data.

Events & transitions

An <on> maps an eventId to a newStateId and an optional action. That's the whole grammar of movement.

📦

State entities

Your object extends StateEntity and carries its current state. Finito persists it for you and can fire hooks after every save.

Much more than XML

Three ways to define the same flow

XML is convenient and reviewable — but it's only one door. Build a flow with a fluent Java API, or load it from JSON. Same engine, same diagrams, your choice of source.

returns-states.xml
<states>
  <flow id='returns-flow' default='true'>
    <entry-action componentName="returnsEntryAction"/>
    <manual-state id='REQUESTED' initialState='true'>
      <on eventId='approve' newStateId='APPROVED' componentName='ApproveReturn'/>
      <on eventId='reject'  newStateId='REJECTED'/>
    </manual-state>
    <auto-state id='INSPECTION' code='payload.condition'>
      <on eventId='ok'      newStateId='REFUNDED'/>
      <on eventId='damaged' newStateId='REJECTED'/>
    </auto-state>
  </flow>
</states>
ReturnsFlow.java — fluent construction (FluentFlowReader)
new FluentFlowReader(store)
  .newFlow("returns-flow").makeDefault()
    .entryAction(new ReturnsEntryAction())
    .manualState("REQUESTED", true)
      .on("approve").transitionAction(new ApproveReturn()).transitionTo("APPROVED").state()
      .on("reject").transitionTo("REJECTED").state()
      .flow()
    .manualState("APPROVED")
      .on("ship").transitionTo("LABEL_SENT").state()
      .flow()
    .autoState("INSPECTION")
      .component(new IfAction<>())
      .property("condition", "payload.condition")
      .on("ok").transitionTo("REFUNDED").state()
      .on("damaged").transitionTo("REJECTED").state()
      .flow()
    .manualState("REFUNDED");
returns-flow.json — flows serialize to / from JSON
{
  "id": "returns-flow",
  "defaultFlow": true,
  "states": [
    { "id": "REQUESTED", "manualState": true, "initialState": true,
      "transitions": [
        { "eventId": "approve", "newStateId": "APPROVED", "componentName": "ApproveReturn" },
        { "eventId": "reject",  "newStateId": "REJECTED" }
      ]},
    { "id": "INSPECTION", "automaticState": true, "code": "payload.condition",
      "transitions": [
        { "eventId": "ok",      "newStateId": "REFUNDED" },
        { "eventId": "damaged", "newStateId": "REJECTED" }
      ]}
  ]
}
One model underneath
XML, the fluent API and JSON all populate the same in-memory flow model in the STMFlowStore. Pick whichever fits — generate flows programmatically with the fluent API, ship them as JSON from a config service, or keep them as reviewable XML.
A flow, at a glance

The returns workflow — watch it run

However you defined it, this is the machine. Below it animates: an entity walks its states as events fire; the auto state routes itself. Under that, the same flow as a static reference.

An entity walks its states as events fire. Press Play.
start manual state auto state (decides itself) terminal state
manual · initialREQUESTED
approve
manualAPPROVED
ship
manualLABEL_SENT
receive
auto · inspectINSPECTION
ok
terminalREFUNDED
damaged
terminalREJECTED

REQUESTED also goes to REJECTED on reject. The auto state INSPECTION evaluates the item's condition and routes itself — no event from the caller.

Everything hangs off the flow

Actions, hooks and persistence — where they belong

A single transition can run a transition action, land in a new state whose entry action fires, persist the entity automatically, and trigger post-save hooks. You declare it; Finito orchestrates it.

Event

proceed(entity, "approve")

🧩

Transition action

runs your business logic

🚪

Entry action

state entered · SLA/timestamps set

💾

Auto-persist

entityStore.store() → Spring repo

🪝

Post-save hook

notify · integrate · audit

🧩

Transition actions

Attach a component to an <on> to run code when an event fires — validate, call a service, mutate the entity. It implements STMTransitionAction.

🚪

Entry & exit actions

Run logic as a state is entered or left. The built-in GenericEntryAction stamps state-entry time and SLA fields, then persists — so timing and storage are automatic.

🔀

Auto states

An auto-state evaluates an OGNL expression on the entity and picks its own transition — decisions live in the flow, resolved by an STMAutomaticStateComputation.

🪝

Post-save hooks

PostSaveHook.execute(start, end, entity, payload) fires after the entity is stored on a transition — the safe place to publish events, notify or integrate.

💾

Automatic persistence

State is saved through an EntityStore that wraps your Spring repository — entering a state stores the entity with no explicit save call in your code.

🔐

Guards & policies

Attach a security-strategy with meta-acls permissions to events, or an enablement-strategy to switch transitions on and off by configuration.

Convention over configuration

Wired to Spring automatically — by name, not by boilerplate

You rarely wire components by hand. Finito's resolver finds your transition actions, auto-state computations and post-save hooks in the Spring context (or any bean factory) using a naming convention built from a prefix, the event or state, and a type suffix.

how a bean name is resolved
// STMTransitionActionResolver
beanName = prefix + Capitalize(eventId) + suffix

// suffix by component type
TRANSITION_ACTION  ->  "Action"
POST_SAVE_HOOK     ->  "PostSaveHook"
AUTO_STATE         ->  "AutoState"

// for a "returns" workflow, event "approve":
returnsApproveAction        // transition action
returnsAssignedPostSaveHook // hook on the ASSIGNED state
returnsInspectionAutoState  // auto-state computation

How the resolver works

  • It first tries an entity-qualified name (prefix + Entity + Event), then falls back to prefix + Event.
  • A context prefix can override per tenant — e.g. a header x-tenant selects a tenant-specific bean, enabling per-tenant actions and hooks.
  • If nothing matches, an optional default action is used — so you only write the components that differ.
  • Any Spring bean with the right name is picked up automatically — no explicit wiring in the flow.
Look at the returns codebase
In the generated returns service, the resolver is configured with the workflow prefix; dropping a Spring bean named returnsApproveAction (or a tenant-scoped variant) is all it takes to hook business logic onto the approve event. The same convention discovers post-save hooks and auto-state computations.
Automatic diagrams

Your diagram is generated — never hand-drawn, never stale

Finito reads the flow and emits state diagrams in both Mermaid and PlantUML. Main-path, auto and orphaned states are colour-coded automatically, so the picture always matches the running machine.

📄

your flow

XML · fluent · JSON

⚙️

Finito generators

StmPumlGenerator · STMMermaidGenerator

🌊

Mermaid

stateDiagram-v2 · renders on the web

🌱

PlantUML

.puml · for docs & CI

returns.mmd · generated Mermaid
stateDiagram-v2
  classDef mainPath fill:Bisque,stroke:Peru,stroke-width:4px;
  classDef autoState fill:PaleGreen,stroke:green,stroke-width:2.5px;
  [*] --> REQUESTED
  REQUESTED --> APPROVED : approve
  REQUESTED --> REJECTED : reject
  APPROVED --> LABEL_SENT : ship
  LABEL_SENT --> INSPECTION : receive
  state INSPECTION <<choice>>
  INSPECTION --> REFUNDED : ok
  INSPECTION --> REJECTED : damaged
  class REQUESTED mainPath
  class INSPECTION autoState
returns.puml · generated PlantUML
@startuml
[*] --> REQUESTED
REQUESTED --> APPROVED : approve
REQUESTED --> REJECTED : reject
APPROVED --> LABEL_SENT : ship
LABEL_SENT --> INSPECTION : receive
state INSPECTION <<choice>>
INSPECTION --> REFUNDED : ok
INSPECTION --> REJECTED : damaged
@enduml

Live Mermaid render of the code above ↓

stateDiagram-v2
  [*] --> REQUESTED
  REQUESTED --> APPROVED : approve
  REQUESTED --> REJECTED : reject
  APPROVED --> LABEL_SENT : ship
  LABEL_SENT --> INSPECTION : receive
  state INSPECTION <<choice>>
  INSPECTION --> REFUNDED : ok
  INSPECTION --> REJECTED : damaged
Trajectories

Run a different flow behaviour — without branching

A trajectory is an ephemeral, per-request variant selected by the header x-chenile-trajectory-id. Because Finito resolves its transition actions and auto-states by convention, a trajectory can point an event at a different action or auto-state computation — no if statements, nothing to untangle later.

request— no trajectory header —
Pick a trajectory — or press Play — to see the resolver pick a different bean.
Same idea, framework-wide
In the full Chenile framework a trajectory also swaps the service implementation (@ConditionalOnTrajectory) and returns different configuration values — read the Trajectories chapter on chenile.org.
The blueprint · jgen

From one flow to a running workflow service

Finito ships an inbuilt blueprint — bp-wfcustom — that jgen expands into a complete Chenile service: an -api and a -service module wired to your state machine, with persistence, tests and diagrams.

📄

your flow

+ a few answers

🧬

jgen · bp-wfcustom

the inbuilt blueprint

📘

returns-api

StateEntity + StateEntityService<T>

⚙️

returns-service

actions, hooks, config, BDD tests

🖼️

diagrams

Mermaid + PlantUML

Standardised contract
Your entity extends AbstractStateEntity; the generated API reuses the shared StateEntityService<T> — so every workflow exposes the same verbs: process, processById, create, retrieve, getAllowedActionsAndMetadata. Toggle JPA, security, activities, enablement and the cloud switch at generation time.
stm-cli

A command line for your state machines

stm-cli works straight off your flow — render its diagram, and even scaffold test cases that walk every transition.

terminal
# render a Mermaid state diagram
stm-cli --mermaid returns-states.xml

# generate test cases that walk every transition
stm-cli --generate-test-cases returns-states.xml

# render a state diagram for each generated test path
stm-cli --render-tests-as-state returns-states.xml
-m, --mermaid

Emit a Mermaid-compatible state diagram for the flow.

-t, --generate-test-cases

Generate test cases that exercise the machine's transitions.

-r, --render-tests-as-state

Draw a state diagram for every generated test-case path.

Where it fits

Part of Chenile — happy on its own

Finito is the state-machine core of the Chenile framework, but the engine has no framework strings attached.

🧩 Standalone engine

Drop org.chenile.stm into any Java app. Build a flow with XmlFlowReader, the FluentFlowReader API, or from JSON, then drive entities with STM.proceed(...).

  • Plain Java — no Spring, no container required
  • Pluggable strategies (scripting, security, store) with no-op defaults
  • Generate Mermaid / PlantUML from any flow with the CLI
  • Perfect for embedding a workflow inside an existing service

🟠 Within the Chenile group

Use the full workflow blueprint and get a first-class Chenile service: convention-wired actions and hooks, Spring persistence, security, activities, BDD tests and REST endpoints.

  • Generated -api + -service via jgen bp-wfcustom
  • Convention-based resolution from the Spring context
  • Shares Chenile's interceptors, registry and messaging
  • Versioned with the Chenile release train

Model your first state machine

Define a flow — in XML, Java or JSON — drive it with one call, let Finito wire and persist it, and draw the diagram for you.