Flower is a small in-JVM orchestration runtime that makes long-running Java and Spring application flows explicit, testable, observable, and operable.
Engine -> Worker -> Flow -> Step -> StepResult
Latest release: 0.1.3. The main branch is developing
0.1.4-SNAPSHOT. The stable center is flower-core; modules marked MVP are
usable but may change more quickly before a 1.0 release. See
CONTRIBUTING.md, SECURITY.md, and
ROADMAP.md for project process and planned work.
Your app has flows. You just cannot see them yet.
Hand your service to a new engineer and watch where they get stuck. Not on the class diagram. That part may be clean. They get stuck on one question no diagram answers: when a request comes in, what actually happens, and in what order?
Because the answer is not in any single place. It is spread across a poller, a background thread, an event listener, a shared flag, and a catch-all service class that quietly became the home for leftover orchestration.
The person who knows the whole sequence will eventually move on, and the flow moves with them.
Flower is how you write that flow down before that happens: one flow, small steps, explicit transitions, and one place to look.
Flower is a tiny in-JVM runtime for long-running Spring application flows. It gives application code an explicit, testable, human-operable execution shape inside one JVM, without replacing your application framework or your domain model.
Engine
-> Worker
-> Flow
-> Step
-> StepResult
A flow has a current step. A step returns an explicit result. Waiting is modeled through events, signals, timeouts, or durable domain state instead of hidden sleeps and ad-hoc polling loops.
That is the whole discipline, borrowed from equipment-control software where long-running work has always been modeled this way: make the current state visible, make every transition explicit, keep each unit small, and leave a trace a human can inspect.
This is the shape the "already there" flow usually starts in: orchestration scattered across a status string, a scheduled scan, and a side-channel flag.
@Entity
class Order {
String id;
String status; // "NEW", "WAITING_PAYMENT", "PAID", "FULFILLED", "FAILED"
Instant paymentDeadline;
}
@Component
class OrderPoller {
@Scheduled(fixedDelay = 1000)
void tick() {
for (Order order : orders.findActive()) {
switch (order.status) {
case "NEW" -> {
prepare(order);
order.status = "WAITING_PAYMENT";
order.paymentDeadline = now().plusSeconds(30);
}
case "WAITING_PAYMENT" -> {
if (paidFlags.contains(order.id)) {
order.status = "PAID";
} else if (now().isAfter(order.paymentDeadline)) {
order.status = "FAILED";
}
}
case "PAID" -> {
fulfill(order);
order.status = "FULFILLED";
}
}
orders.save(order);
}
}
}
@EventListener
void onPaid(PaymentApproved event) {
paidFlags.add(event.orderId());
}The state machine is implicit in strings and a switch. Waiting is hidden in a shared flag. Recovery means "whatever the status column happened to be." Nobody can see the flow, because there is no flow, only fragments.
After: the same behavior as an explicit Flow of Steps:
Flow flow = Flow.builder("order", orderId)
.step("accept", new AcceptOrderStep(orderService))
.step("payment", new WaitForPaymentStep())
.step("fulfill", new FulfillOrderStep(warehouseService))
.build();
worker.submit(flow);final class WaitForPaymentStep extends Step {
@Override
protected void onEnter(StepContext ctx) {
ctx.startTimeout(30_000);
ctx.subscribe(PaymentApproved.class, event -> {
if (event.orderId().equals(ctx.flowId().flowKey())) {
ctx.signal("paid");
}
});
}
@Override
protected StepResult onTick(StepContext ctx) {
if (ctx.hasSignal("paid")) {
return StepResult.done();
}
if (ctx.timedOut()) {
return StepResult.fail(new IllegalStateException("payment timeout"));
}
return StepResult.stay();
}
}Now the current step is visible. The transition is the return value. The wait
is a subscription plus a timeout that Flower cleans up for you. The same flow
can be tested with a manual clock and tickOnce(), without starting a
scheduler or database. That is the readable shape a new engineer can follow
and repair.
Flower's Worker -> Flow -> Step -> StepResult execution model was shaped by
practical experience gained while developing industrial equipment control
systems and business applications.
It generalizes recurring patterns observed in long-running, stage-based processes into a reusable workflow runtime for Java applications: explicit execution stages, result-driven transitions, waits, timeouts, retries, human intervention, and inspectable execution traces.
The underlying discipline is simple: make the current state visible, make transitions explicit, keep each unit of work small, and leave a trace that a human can inspect.
Use Flower when application work has multiple phases and should progress over time or in response to events:
- order processing that waits for payment, inventory, or fulfillment signals
- game turns where a flow waits for player input and animation completion
- logistics or device workflows where each unit of work moves through zones
- retryable background coordination that should remain testable
- AI agent applications that coordinate model turns, tools, context, and governed domain actions
- AI-assisted work that waits for model, tool, approval, or action results
- demos and simulations that need deterministic manual ticks
Flower makes this kind of logic easier to reason about because every flow has a
current step, every step returns an explicit result, and time/event waiting is
represented by StepContext instead of ad-hoc threads and sleeps.
Flower is not an LLM SDK, but it provides a useful execution structure around AI work. Model calls, tool waits, validation, approvals, governed actions, and operator intervention can be represented as visible Flow phases instead of a hidden callback chain:
prepare context
-> run model or agent
-> wait for tools
-> validate result
-> request approval when required
-> execute governed action
-> observe final state
The wider Flower JVM ecosystem keeps those responsibilities separate:
| Project | AI automation responsibility |
|---|---|
| Flower | Runs the surrounding application Flow and explicit waits. |
| Flower Agent | Owns AgentRun, model turns, transcripts, tool calls, budgets, and completion. |
| Flower AI Harness | Validates final structured output and controls whole-task refinement or retry. |
| Flower Action Runtime | Governs mutating actions with policy, approval, idempotency, and audit. |
See Flower Agent Samples for a runnable Spring Boot application that combines these layers with an OpenAI-compatible cloud or local model.
Flower also helps when coding agents generate ordinary application code. The
small Flow -> Step -> StepResult contract gives human reviewers and coding
agents one execution shape to inspect. The Flower plugin provides guidance
before generation, flower-check detects known misuse during the build, and
deterministic tests verify behavior afterward. Flower structures the generated
or hand-written orchestration; it does not replace the coding agent itself.
Once the flow is visible, it is worth being precise about scope.
Flower is not BPMN, Temporal, Camunda, a distributed scheduler, or a durable saga engine. It stays in one JVM on purpose. If you need cross-service distributed transactions, durable execution replay, a BPMN designer, or a multi-node scheduler, reach for those tools. That is not what Flower is for.
For small flows, an enum and a switch are genuinely enough. Use them. Spring StateMachine is a good fit when your main problem is modeling formal states, events, transitions, and guards.
Flower is for the other case: your domain model stays in your Spring Boot application, but a long-running internal flow needs a small runtime to execute it, one that waits for events, handles timeouts, checkpoints, resumes, inspects, and tests inside one JVM. State machines model state. Flower runs flows.
The cost of "just build it yourself" is that, one requirement at a time, you rebuild a runtime you did not mean to write.
| What the flow eventually needs | Hand-rolled around an enum | With Flower |
|---|---|---|
| Wait for an event, then clean up the subscription | Register/deregister listeners by hand; leaks are easy to miss. | ctx.subscribe(...) in onEnter, released automatically on exit/reset/finish. |
| Timeout on a wait | Deadline field plus a scheduler that checks it. | ctx.startTimeout(30_000) and ctx.timedOut(). |
| Retry or explicit failure transition | Extra state, counters, and branches in the switch. | StepResult.repeat() / StepResult.fail(cause). |
| Checkpoint and resume after restart | Serialize position, persist it, rebuild, and resume. | durable() plus a FlowCheckpointStore. |
| Deterministic tests | Abstract the clock, bypass the scheduler, and fake the bus yourself. | ManualClock plus worker.tickOnce(). |
| Inspect what is running right now | Build your own dump/admin view. | Engine.dump() plus optional console. |
import io.github.flowerjvm.flower.core.engine.Engine;
import io.github.flowerjvm.flower.core.event.InMemoryEventBus;
import io.github.flowerjvm.flower.core.flow.Flow;
import io.github.flowerjvm.flower.core.step.Step;
import io.github.flowerjvm.flower.core.step.StepContext;
import io.github.flowerjvm.flower.core.step.StepResult;
import io.github.flowerjvm.flower.core.time.SystemClock;
import io.github.flowerjvm.flower.core.worker.Worker;
public final class FlowerQuickStart {
static final class PrepareOrderStep extends Step {
@Override
protected StepResult onTick(StepContext ctx) {
System.out.println("prepare " + ctx.flowId());
return StepResult.done();
}
}
static final class CompleteOrderStep extends Step {
@Override
protected StepResult onTick(StepContext ctx) {
System.out.println("complete " + ctx.flowId());
return StepResult.done();
}
}
public static void main(String[] args) throws Exception {
Worker worker = Worker.builder("orders")
.intervalMillis(100)
.build();
Engine engine = Engine.builder()
.clock(SystemClock.INSTANCE)
.eventBus(InMemoryEventBus.create())
.worker(worker)
.build();
Flow flow = Flow.builder("order", "order-1")
.step("prepare", new PrepareOrderStep())
.step("complete", new CompleteOrderStep())
.build();
engine.start();
worker.submit(flow);
Thread.sleep(500);
engine.stop();
}
}For deterministic tests, use engine.attach() and worker.tickOnce() instead
of starting the scheduler.
Worker worker = Worker.builder("test").build();
Engine engine = Engine.builder()
.eventBus(InMemoryEventBus.create())
.worker(worker)
.build();
engine.attach();
worker.submit(flow);
worker.tickOnce();
worker.tickOnce();Flower 0.1.3 is published to Maven Central under the
io.github.flowerjvm group. No custom repository or mavenLocal() is
required.
For a plain Java application, start with flower-core:
Gradle Kotlin DSL:
dependencies {
implementation("io.github.flowerjvm:flower-core:0.1.3")
}Maven:
<dependency>
<groupId>io.github.flowerjvm</groupId>
<artifactId>flower-core</artifactId>
<version>0.1.3</version>
</dependency>For Spring Boot applications, use the starter instead:
dependencies {
implementation("io.github.flowerjvm:flower-spring-boot-starter:0.1.3")
}Add only the modules your application needs:
| Use case | Artifact |
|---|---|
| Core Flow / Worker runtime | io.github.flowerjvm:flower-core:0.1.3 |
| Spring Boot auto-configuration | io.github.flowerjvm:flower-spring-boot-starter:0.1.3 |
| JDBC checkpoints | io.github.flowerjvm:flower-persistence-jdbc:0.1.3 |
| Logging, metrics, tracing, and dumps | io.github.flowerjvm:flower-observability:0.1.3 |
| Offline datasets, experiments, and evaluators | io.github.flowerjvm:flower-evaluation:0.1.3 |
| Deterministic test helpers | io.github.flowerjvm:flower-testkit:0.1.3 |
| Event-driven execution | io.github.flowerjvm:flower-eventloop:0.1.3 |
| Event-loop JDBC checkpoints | io.github.flowerjvm:flower-eventloop-persistence-jdbc:0.1.3 |
See Modules And Maturity before adopting an MVP
module. The Bloom adapter is published separately as
io.github.flowerjvm:bloom-flower-adapter:0.1.1.
| Build or artifact | Minimum Java |
|---|---|
flower-core, event-loop, persistence, observability, evaluation, testkit, and Flower Check artifacts |
Java 8 |
flower-spring-boot-starter |
Java 17 and Spring Boot 3.x |
| Full Flower repository build | JDK 17 |
CI runs the Java 8-compatible modules on a Java 8 runtime and verifies the full reactor on JDK 17 and 21. A Java 8/11 application can use the compatible artifacts, but it cannot load the Spring Boot starter because that artifact is compiled for Java 17.
Build-time Flower usage checks are available through the
flower-check-maven-plugin and
flower-check-gradle-plugin, both at
version 0.1.3.
Install the Flower plugin for ChatGPT and Codex. Coding agents can build, verify, and maintain Flower workflows directly in your Java project. The plugin includes guidance for Flower application workflows and governed actions with Flower Action Runtime.
Flower works well when Kafka tells a Spring Boot service that something happened and the service needs to advance an internal flow.
Kafka tells the application that something happened. Flower decides whether the current step can move forward. The database remembers the business fact.
This example keeps Kafka concerns such as duplicate handling, inbox/outbox, and startup recovery out of the main flow. Those belong in production code, not in the first shape.
@Component
final class OrderKafkaListener {
private final Engine engine;
private final OrderRepository orders;
private final OrderFlowFactory flows;
@KafkaListener(topics = "order-created")
void onOrderCreated(OrderCreated event) {
orders.markCreated(event.orderId());
engine.worker("orders").submit(
flows.createOrderFlow(event.orderId()),
DuplicatePolicy.IGNORE);
}
@KafkaListener(topics = "payment-approved")
void onPaymentApproved(PaymentApproved event) {
orders.markPaymentApproved(event.orderId());
engine.eventBus().publish(event);
}
}
final class OrderFlowFactory {
private final OrderRepository orders;
OrderFlowFactory(OrderRepository orders) {
this.orders = orders;
}
Flow createOrderFlow(String orderId) {
return Flow.builder("order", orderId)
.step("accept", new AcceptOrderStep())
.step("payment", new WaitPaymentStep(orders))
.step("complete", new CompleteOrderStep())
.build();
}
}
final class WaitPaymentStep extends Step {
private final OrderRepository orders;
WaitPaymentStep(OrderRepository orders) {
this.orders = orders;
}
@Override
protected void onEnter(StepContext ctx) {
ctx.startTimeout(30_000);
ctx.subscribe(PaymentApproved.class, event -> {
if (event.orderId().equals(ctx.flowId().flowKey())) {
ctx.signal("paid"); // event arrived; check the DB on the next tick
}
});
}
@Override
protected StepResult onTick(StepContext ctx) {
String orderId = ctx.flowId().flowKey();
if (orders.isPaymentApproved(orderId)) {
return StepResult.done();
}
if (ctx.timedOut()) {
return StepResult.fail(new IllegalStateException("payment timeout"));
}
return StepResult.stay();
}
}The event handler does not complete the Step directly. It records a signal, and
the next onTick completes the Step by returning StepResult.done() after the
database says the payment is approved.
The split is simple:
Kafka event = something happened
Flower Step = decide stay, done, or fail
Database = remember the business fact
In a Spring multi-module application, Flower usually belongs in the workflow module rather than the domain model itself:
order-api REST/Kafka input
order-domain Order, OrderStatus, repository, domain service
order-workflow Flower FlowFactory and Step classes
order-events Kafka event DTOs, publisher, listener
order-infra DB, Kafka, Flower engine config
The Kafka listener stays thin: persist the domain fact, publish the event to Flower's in-JVM event bus, and let the Step decide whether the flow can advance.
Keep the boundaries boring on purpose:
- Kafka carries domain events.
- Flower keeps the internal execution position.
- The DB keeps business facts and recovery state.
- Flower signals are hints, not business facts.
- Use an inbox or event id check for duplicate Kafka events when needed.
- Use an outbox for external events or commands that must be published reliably.
- On startup, recover or submit flows for DB records that are still active but not currently running.
Flower core is deliberately small, so its runtime contract is also explicit:
- Concurrency: a Worker ticks its Flows on one scheduler thread. Submit/cancel
requests are queued. Event callbacks may call
ctx.signal(...); do not mutate Step fields directly from callback threads. - Recovery: durable Flows checkpoint the current step id,
stepNo, execution context, and definition version. Recovery rebuilds a fresh Flow and resumes from that checkpoint. It is not deterministic replay or exactly-once side effect execution, so external writes and API calls should be idempotent. - Durable event-loop effects have explicit crash windows. An
await(...).thenRunorthenPublisheffect runs after the await checkpoint, so a process failure in between can leave the saved wait without having dispatched the effect. Effects attached tonext,goTo,finish, orfailrun before the next or terminal checkpoint, so a recovered application may repeat them. Important external work needs a durable intent/outbox or operation record with a stable idempotency key; Flower does not make either ordering exactly-once. - Scale: the default Worker is tick-based and simple to test. It is a good fit for small to medium in-process workloads. Very large numbers of idle Flows may need application-level sharding or a different Worker scheduling strategy. Possible scheduling optimizations are tracked in ROADMAP.md.
Engine
-> Worker
-> Flow
-> Step
-> StepResult
-> stepNo
Engine: top-level runtime. OwnsClock,EventBus,Workers, and listeners.Worker: single-threaded tick loop. Owns active flows and ticks each non-terminal flow once per worker tick.Flow: one ordered sequence of steps for one domain instance, identified byFlowId(flowType, flowKey).Step: a small stateful orchestration unit. It receives aStepContextand returnsStepResult.StepResult: the explicit transition returned by a Step.stepId: a stable flow-level string id used bygoTo, dumps, checkpoints, and admin views.stepNo: optional step-local cursor for tiny sub-state inside one step.
Flower core is not an AI framework, and it does not depend on an LLM. Its relevance in the AI coding era is structure.
AI can generate more orchestration code than humans can comfortably review when that code becomes scattered callbacks, service methods, scheduled jobs, and background threads. Flower's contribution is to force that behavior into a small, inspectable shape:
Engine -> Worker -> Flow -> Step -> StepResult
Generated and hand-written orchestration both become easier to inspect, test, recover, observe, and change. A step starts work, checks state, and returns an explicit result, so a reviewer, tool, or coding agent can follow it.
flower-check is available as build-time tooling for host applications. It can
reject known Flower anti-patterns such as blocking a worker tick or hiding
orchestration outside the Flow / Step boundary. Longer-term developer tooling
ideas live in ROADMAP.md; they are intentionally outside
flower-core.
onEnter(ctx) called once when the step becomes current
onTick(ctx) called once per worker tick while the step is current
onExit(ctx) called when the step leaves by done, goTo, finish, or fail
onReset(ctx) called for StepResult.repeat(), then the step re-enters
onTick returns one of:
| Result | Meaning |
|---|---|
StepResult.stay() |
Keep this step and tick again later. |
StepResult.done() |
Finish this step and move to the next declared step, or finish the flow if this was the last step. |
StepResult.repeat() |
Reset this step and run it from the beginning. |
StepResult.goTo("stepId") |
Jump to another flow-level step id. |
StepResult.finish() |
Finish the flow successfully without running later steps. |
StepResult.fail(Throwable) |
Fail the flow. |
Flower already has step ids. They are flow-level string ids:
Flow flow = Flow.builder("order", orderId)
.step("accept", new AcceptOrderStep(orderService))
.step("payment", new WaitForPaymentStep())
.step("fulfill", new FulfillOrderStep(warehouseService))
.build();
return StepResult.goTo("payment");The core keeps step ids as strings because the same ids must be readable in logs, dumps, checkpoints, admin screens, and external configuration. If you want type safety in application code, wrap them with an enum:
enum OrderStep {
ACCEPT("accept"),
PAYMENT("payment"),
FULFILL("fulfill");
private final String id;
OrderStep(String id) {
this.id = id;
}
String id() {
return id;
}
}Flow flow = Flow.builder("order", orderId)
.step(OrderStep.ACCEPT.id(), new AcceptOrderStep(orderService))
.step(OrderStep.PAYMENT.id(), new WaitForPaymentStep())
.step(OrderStep.FULFILL.id(),
new FulfillOrderStep(warehouseService))
.build();Use stepNo only as a small cursor inside one Step. If it starts representing
business states such as WAITING_PAYMENT, RETRYING, FULFILLING, or
FAILED, split the behavior into explicit Steps.
When multiple Steps need shared values, do not hide them in stepNo or
step-local signals. Use domain state, or pass a small run context object while
building the Flow:
final class OrderFlowRun {
final String orderId;
PaymentResult paymentResult;
FulfillmentPlan fulfillmentPlan;
OrderFlowRun(String orderId) {
this.orderId = orderId;
}
}OrderFlowRun run = new OrderFlowRun(orderId);
Flow flow = Flow.builder("order", orderId)
.step(OrderStep.ACCEPT.id(), new AcceptOrderStep(run, orderService))
.step(OrderStep.PAYMENT.id(), new WaitForPaymentStep(run, paymentService))
.step(OrderStep.FULFILL.id(),
new FulfillOrderStep(run, warehouseService))
.build();For durable flows, keep recoverable business state in your domain storage. A run context object is convenient for transient coordination, but it is not a durable source of truth after process restart.
Steps should be asynchronous in shape. Do not block a worker thread while
waiting for outside work. Start or subscribe in onEnter, return stay() while
waiting, and return done() when a signal or timeout says the work is ready.
final class WaitForPaymentStep extends Step {
@Override
protected void onEnter(StepContext ctx) {
ctx.startTimeout(30_000);
ctx.subscribe(PaymentApproved.class, event -> {
if (event.orderId().equals(ctx.flowId().flowKey())) {
ctx.signal("paid");
}
});
}
@Override
protected StepResult onTick(StepContext ctx) {
if (ctx.hasSignal("paid")) {
return StepResult.done();
}
if (ctx.timedOut()) {
return StepResult.fail(new IllegalStateException("payment timeout"));
}
return StepResult.stay();
}
}If onTick needs the event data, attach it to the signal. Flower keeps only
the latest payload for each signal name, which is usually what a waiting step
needs:
ctx.subscribe(PaymentApproved.class, event -> ctx.signal("paid", event));
PaymentApproved approved = ctx.consumeSignal("paid", PaymentApproved.class);
if (approved != null) {
return StepResult.done();
}Subscriptions made through StepContext.subscribe(...) are cleaned up
automatically when the step exits, resets, or the flow terminates.
You may also unsubscribe a specific event while a step is still running. Keep that pattern small. If the step starts to need its own large internal state machine, split the behavior into multiple explicit Steps instead.
- Keep
onTickshort and non-blocking. Nosleep, long polling, network waits, or database loops inside the worker tick. - Start external work in
onEnter, then observe completion through events, signals, stored domain state, or timeouts. - Use
StepContext.subscribe(...)for step-owned event subscriptions so Flower can release them automatically. - Use
StepContext.eventBus().publish(...)when a step needs to emit an event. - Use
stepNofor small internal cursors, not for large hidden state machines. If the cursor turns into business state, split the Step. - Put heavy domain logic in services. A step should orchestrate, not become the domain model.
- Pass dependencies through step constructors. Flower does not instantiate steps by reflection and does not provide a DI container in core.
- Give every step a stable, meaningful flow-level id.
goTo(...)targets that id, not the Java class name. Wrap ids in an enum when application code needs type safety. - Keep shared values in domain state or an explicit run context object. Durable flows must be recoverable from domain state and checkpoints, not from transient Step fields alone.
- Prefer immutable events and exact event classes. The default event buses match by exact runtime type.
- Treat a
Stepinstance as owned by oneFlow. Create fresh step instances when building a new flow. - Use
Guardfor "do not enter this step yet" rules. UseStepResult.stay()for "I entered and am waiting" rules. - Make terminal outcomes explicit. Return
done()for success andfail(cause)for failure.
Flow flow = Flow.builder("order", orderId)
.step("accept", new AcceptOrderStep(orderService))
.step("payment", new WaitForPaymentStep())
.step("fulfill", new FulfillOrderStep(warehouseService))
.build();
worker.submit(flow);flowType and flowKey form the FlowId. Submitting a duplicate flow defaults
to DuplicatePolicy.REJECT. You can also use IGNORE or REPLACE.
worker.submit(flow, DuplicatePolicy.REPLACE);A FlowId(flowType, flowKey) answers "which domain instance is this flow for?"
ExecutionContext answers "whose execution is this?"
Use it when logs, dumps, checkpoints, admin views, or future audit/eval tooling need to connect one flow run to a tenant, user, session, run id, trace id, or correlation id.
Existing code does not need to change. Flows default to
ExecutionContext.empty().
import io.github.flowerjvm.flower.core.context.ExecutionContext;
ExecutionContext execution = ExecutionContext.builder()
.tenantId("office-a")
.userId("user-1")
.sessionId("session-1")
.runId("run-123")
.traceId("trace-abc")
.correlationId("request-789")
.build();
Flow flow = Flow.builder("order", "ORD-1")
.executionContext(execution)
.step("accept", new AcceptOrderStep(orderService))
.step("payment", new WaitForPaymentStep())
.build();Steps can read it when they need execution identity:
String tenantId = ctx.executionContext().tenantId().orElse("default");
String runId = ctx.executionContext().runId().orElse("unknown");Keep this context small. It is an execution id card, not a business context. Do not put roles, permissions, approval state, domain objects, agent ids, action ids, or policy decisions in Flower core context. Keep that state in the host application's own runtime or in a higher-level integration layer.
ExecutionContext is attached to the Flow, not to a ThreadLocal. That keeps
the same identity visible from steps, listeners, dumps, checkpoints, and
recovery even when events or callbacks happen on other threads.
Important: tenantId does not change Flower's duplicate-flow identity.
Worker still uses only FlowId(flowType, flowKey) to detect duplicates. If
two tenants can have the same domain key, make the flowKey globally unique in
the host application, for example office-a:DOC-1.
flower-core includes InMemoryEventBus for simple setups and deterministic
tests. Bloom is the small in-memory event bus provided in the Flower ecosystem.
To share events with Bloom, use Bloom's optional bloom-flower-adapter module:
EventBus bloom = LocalEventBus.create();
Engine engine = Engine.builder()
.eventBus(BloomEventBus.wrap(bloom))
.worker(Worker.builder("main").build())
.build();The adapter preserves the dispatch semantics of the wrapped Bloom bus. The adapter is owned by the Bloom repository so Flower's default build remains independent of Bloom.
When Flower is backed by Bloom, application code can publish to Bloom directly.
Flower steps subscribed through ctx.subscribe(...) will receive the same
events.
EventBus bloom = LocalEventBus.create();
Engine engine = Engine.builder()
.eventBus(BloomEventBus.wrap(bloom))
.worker(Worker.builder("orders").intervalMillis(100).build())
.build();Application code publishes an ordinary Bloom event:
final class PaymentService {
private final EventBus bloom;
private final OrderRepository orders;
PaymentService(EventBus bloom, OrderRepository orders) {
this.bloom = bloom;
this.orders = orders;
}
void approvePayment(String orderId) {
orders.markPaymentApproved(orderId); // business fact
bloom.publish(new PaymentApproved(orderId)); // wake waiting steps
}
}The waiting Flower step receives that Bloom event through the adapter:
final class WaitPaymentStep extends Step {
private final OrderRepository orders;
WaitPaymentStep(OrderRepository orders) {
this.orders = orders;
}
@Override
protected void onEnter(StepContext ctx) {
ctx.startTimeout(30_000);
ctx.subscribe(PaymentApproved.class, event -> {
if (event.orderId().equals(ctx.flowId().flowKey())) {
ctx.signal("payment-approved");
}
});
}
@Override
protected StepResult onTick(StepContext ctx) {
if (orders.isPaymentApproved(ctx.flowId().flowKey())) {
return StepResult.done();
}
if (ctx.timedOut()) {
return StepResult.fail(new IllegalStateException("payment timeout"));
}
return StepResult.stay();
}
}In this setup Bloom remains the application event bus, while Flower uses the same events to advance the internal flow. The signal is only a wake-up hint; the database remains the source of truth.
flower-spring-boot-starter auto-configures:
- a
Clockbean, defaulting toSystemClock.INSTANCE - an
EventBusbean, defaulting toInMemoryEventBus - a
FlowCheckpointStore, when JDBC persistence is explicitly enabled - an
Enginebean - a lifecycle bean that starts and stops the engine with the application context
Example configuration:
flower:
enabled: true
auto-start: true
persistence:
type: none
workers:
- name: orders
interval-ms: 100
- name: alerts
interval-ms: 250Provide your own Engine, EventBus, Clock, or FlowerListener beans when
you need more control. The auto-configuration backs off where appropriate.
For durable flows with JDBC checkpoints, add flower-persistence-jdbc, create
the table using the packaged schema SQL, and enable the store explicitly:
flower:
persistence:
type: jdbc
jdbc:
dialect: postgresql
initialize-schema: neverSupported dialects are postgresql, mysql, oracle, and h2. The starter
does not create tables automatically; initialize-schema is reserved and
currently only supports never. If you need a custom backend, provide a
FlowCheckpointStore bean and the auto-configured Engine will use it.
Flower's durable mode is checkpoint/resume, not durable execution replay. It stores only the current Flow position so an application can rebuild a fresh Flow and resume ticking from that position.
Flow flow = Flow.builder("order", orderId)
.durable()
.durableStep("payment", new WaitPaymentStep(orderService),
RecoveryPolicy.REENTER_IDEMPOTENT)
.durableStep("fulfill", new FulfillOrderStep(warehouseService),
RecoveryPolicy.REENTER_IDEMPOTENT)
.build();Durable flows require every step to declare a recovery policy. A regular
Step may opt in through durableStep(...) with
RecoveryPolicy.REENTER_IDEMPOTENT when re-running onEnter is safe. If
initial entry and recovery setup must be different, extend DurableStep with
RecoveryPolicy.RESUME_ONLY and implement onResume(ctx).
Flow recovered = Flow.builder("order", orderId)
.durable()
.durableStep("payment", new WaitPaymentStep(orderService),
RecoveryPolicy.REENTER_IDEMPOTENT)
.durableStep("fulfill", new FulfillOrderStep(warehouseService),
RecoveryPolicy.REENTER_IDEMPOTENT)
.build()
.recoverFrom(checkpoint);Applications that want a small startup helper can register factories by
flowType and recover the checkpoints they choose:
FlowFactoryRegistry registry = FlowFactoryRegistry.builder()
.register("order", id -> buildOrderFlow(id.flowKey()))
.build();
FlowRecoveryService recovery = FlowRecoveryService.create(store, registry);
recovery.recoverActiveForWorker(engine.worker("orders"));The helper only rebuilds fresh Flows and submits them to the chosen Worker. It does not start Workers, create schema, lock rows, delete failed checkpoints, or turn Flower into an event replay engine.
Core exposes FlowCheckpointStore as the storage boundary. The default store
is no-op, so existing transient flows are unaffected. Core does not create DB
tables. JDBC, Redis, JPA, or file-backed checkpoint stores should live in
optional modules or in the host application, and schema initialization should
be explicit and opt-in.
Durable checkpoints keep the ExecutionContext with the saved flow position.
After recovery, the same logical run keeps the same runId, traceId, tenant,
and user identifiers. Flower does not regenerate a new run id during recovery.
Core StepContext.startTimeout(...) is a runtime-only helper and is not stored
in durable checkpoints. Durable Flows reject it so a restart cannot silently
reset or lose a deadline. For durable waits, store dueAtMillis or equivalent
deadline data in domain state, or use the event-loop runtime's await deadlines.
flower-persistence-jdbc provides a JDBC implementation:
FlowCheckpointStore store = JdbcFlowCheckpointStore.create(
dataSource,
JdbcCheckpointDialects.postgresql());
Engine engine = Engine.builder()
.eventBus(InMemoryEventBus.create())
.worker(Worker.builder("orders").build())
.checkpointStore(store)
.build();flower-eventloop-persistence-jdbc provides a separate JDBC implementation for
event-loop checkpoints. Schema SQL is packaged for PostgreSQL, MySQL, Oracle,
H2, and SQLite. Apply the SQL yourself, or copy it into Flyway/Liquibase. The
JDBC stores do not create tables automatically.
SQLite support is aimed at embedded desktop and agent applications that ship a
local database file. The host supplies the SQLite JDBC driver and may use the
same DataSource for its own tables and both Flower checkpoint stores. See
Persistence for the SQLite dialect, schema paths, and
single-process operating guidance.
For dialect paths, execution-context columns, and migration notes, see Persistence.
Signals are still in-memory wake-up hints. Durable step decisions should be based on domain state that can be checked again after restart, not on signal payloads alone.
Operational boundaries to remember:
- Flow ownership is enforced inside one
Engine, not across JVMs. If multiple processes recover from the same checkpoint store, the application must coordinate recovery with its own lock, lease, or leader election. - Checkpoint
save(...)anddelete(...)run synchronously on the Worker tick path or EventWorker loop path. Slow storage slows Flow progress. - Terminal durable Flows save a terminal tombstone before cleanup delete, so normal completion may perform both a save and a delete.
definitionVersionis checked only when both the Flow and checkpoint have a non-null version.
Attach FlowerListener implementations to observe flow submission, step
entry/exit, flow completion, cancellation, failure, listener errors, and worker
errors. Engine.dump() gives a snapshot of the current engine and worker
state, including active flows, current step id, current step index, current
stepNo, and the declared step list for admin/console views.
The 0.1.2 runtime also emits payload-light FlowerTraceEvent
records to opt-in FlowerTraceListener implementations. Unlike the older
coarse lifecycle callbacks, these events include per-runtime sequence numbers,
distinct stepRunId values for repeated Step attempts, effective transition
outcomes, event-loop wait/resume reasons, and durable checkpoint/recovery
facts. Existing FlowerListener implementations remain unchanged and do not
pay Trace allocation cost.
flower-observability provides FlowerTraceSinkListener, in-memory and
composite sinks, AsyncFlowerTraceSink, OpenTelemetryFlowerTraceSink, and an
append-only JsonLinesFlowerTraceSink. Storage and security adapters include
fail-closed TraceSanitizer, deterministic trace-level sampling, explicit
content capture policy, and a local content-addressed artifact store.
For cross-project traces, FlowerObservationEvent is the common envelope and
FlowerObservationSink is the shared destination. FlowerTraceObservationSink
maps Core Flow events into it; separate Agent, AI Harness, and Action Runtime
modules map their native lifecycle events without adding those domain types to
Flower Core.
Keep fast sanitization and sampling on the listener path, then hand selected events to the bounded asynchronous sink before file, artifact, database, HTTP, OpenTelemetry, or messaging I/O:
Worker -> sanitize -> sample -> bounded async queue -> content/artifact -> storage
Flower Core never captures prompts, Tool results, business payloads, or API keys. Higher layers must opt in to content capture and configure their own sanitization policy. Monitor each sink's drop/failure counters; trace backpressure never stops business Flow execution.
Flower Studio is the read-only
local consumer for this correlated stream. It
can open common observation JSON Lines or legacy Core trace JSON Lines and show
Trace outcomes, nested runs, event timing, Step transitions, waits, recovery,
and optional Agent, Harness, Tool, approval, and Action overlays. It also reads
flower-evaluation result and feedback streams to show candidate quality,
cases, scores, baseline regressions, and Trace references. Its Monitoring view
adds bounded Trace outcomes, operation failure/duration, Step-transition,
activity, source, token, approval, and evaluation-quality aggregates. It can
also overlay a selected Core Flow run on an optional static
flower.flow-graph/v4 snapshot, keeping declared structure, observed paths,
version mismatches, and runtime-only transitions visibly distinct. These views
describe the currently loaded local files; production metrics and alerts remain
the job of Micrometer/OpenTelemetry and the host observability platform. See the
Flower Studio repository for the
included runnable demo and its local-reference operating boundary.
flower-evaluation supplies versioned Dataset, Example, Candidate, Experiment,
Evaluator, Score, and Feedback contracts, deterministic rules, failure-isolated
offline execution, baseline comparison, and local JSON Lines reference stores.
It evaluates completed behavior; it does not replace AI Harness validation,
Agent loops, or Action Runtime authorization. See the
Flower Evaluation README.
See Tracing, Studio, And Evaluation Architecture for the event contract and phased Studio/evaluation plan, and Trace Storage And Security for the reference pipeline and operating boundaries. Domain Observation Adapters shows how Flow, Agent, Harness, and Action events share one correlated stream.
Lifecycle listener snapshots stay lightweight. The declared step list is only materialized for dump/admin views so observability does not add work to every listener callback.
flower-spring-boot-starter can expose a read-only Engine dump endpoint when
the application is already a Spring MVC web application. It is disabled by
default because dump output can include flow keys, execution context, and
operational state.
flower:
admin:
dump:
enabled: true
path: /internal/flower/dump
pretty: falseWith the default path, the endpoint is:
GET /internal/flower/dump
GET /internal/flower/dump?pretty=true
The endpoint uses the host application's web server. Flower does not start a separate console server. In production, keep this endpoint behind application authentication, a private network, VPN, or an admin gateway.
For a small built-in web view, enable the console endpoint:
flower:
admin:
console:
enabled: true
path: /internal/flower/console
api-path: /internal/flower/console/dump
poll-interval-ms: 3000
flow-graph-url: http://localhost:8790/Then open:
GET /internal/flower/console
The console is served by the same Spring Boot application and polls the
same-origin api-path. It shows Engine, Worker, Flow, current Step, stepNo,
declared Step order, and execution context. The UI has Start, Stop, Refresh,
and polling interval controls. Its Flow Graph button opens the configured
read-only local source graph in a new tab. The Console itself does not analyze
or serve source code. Use the optional development-only graph starter to manage
the loopback server with the Spring lifecycle, or run the Maven plugin or CLI
separately. Set flow-graph-url to an empty string to hide the button.
Example runtime view:
To start the local graph with a Spring Boot development profile, add the separate Flower Flow Graph starter:
<dependency>
<groupId>io.github.flowerjvm</groupId>
<artifactId>flower-flow-graph-spring-boot-starter</artifactId>
<version>0.1.0</version>
<scope>runtime</scope>
</dependency>flower:
flow-graph:
enabled: true
project-root: .
port: 8790The graph starter is disabled by default and binds only to the loopback interface. Omit it or leave it disabled in production.
This is an internal/admin surface, not a public endpoint. Do not expose it directly to the internet.
The main stable center is flower-core. Everything else orbits it.
Core:
flower-core: stable center. Engine, Worker, Flow, Step, event bus, clock, and listener APIs.
Persistence / integration:
flower-persistence-jdbc: JDBCFlowCheckpointStoreplus schema SQL for PostgreSQL, MySQL, Oracle, H2, and SQLite.flower-spring-boot-starter: Spring Boot auto-configuration for anEngineand optional checkpoint store wiring.
Bloom integration:
bloom-flower-adapter: maintained in the Bloom repository; adapts Bloom's event bus to Flower'sEventBusSPI.
Observability / testing:
flower-observability: listeners and helpers for logging, dumps, metrics, tracing, and awaiting flow completion.flower-studio(separate project): read-only local JSON Lines Trace, Run, and event explorer with optional artifact links, evaluation views, execution graphs, and a bounded monitoring dashboard.flower-evaluation(MVP): post-run datasets, experiments, evaluators, baseline regression comparison, feedback, and local JSON Lines stores.flower-testkit(MVP): deterministic Flow test helpers.
Developer tooling:
flower-check(MVP): build-time Flower usage checker for host applications.flower-check-annotations(MVP): SOURCE-retained approval markers consumed byflower-check.flower-check-maven-plugin(MVP): Mavenverifyintegration forflower-check.flower-check-gradle-plugin(MVP): companion Gradle plugin project for runningflower-checkfrom Gradlecheck.
Early execution line:
flower-eventloop(MVP): separate event-driven runtime for explicit waits such as callbacks, signals, approvals, LLM/tool responses, and deadlines.flower-eventloop-persistence-jdbc(MVP): JDBCEventFlowCheckpointStoreplus event-loop schema SQL.
Read the MVP labels literally. These modules are useful enough to try, but
their APIs may move more than flower-core. The event loop is a separate
execution line, not a replacement for the tick-driven Worker / Flow / Step
model. Work that is not shipped in this repository is tracked in
ROADMAP.md.
flower-testkit keeps testing helpers outside flower-core. It does not
change the runtime model; it only bundles the setup most tests repeat:
Engine + Worker + ManualClock + InMemoryEventBus
+ RecordingFlowerListener + FakeCheckpointStore
Add it as a test dependency:
<dependency>
<groupId>io.github.flowerjvm</groupId>
<artifactId>flower-testkit</artifactId>
<version>0.1.3</version>
<scope>test</scope>
</dependency>Example:
FlowTestHarness harness = FlowTestHarness.create();
Flow flow = Flow.builder("order", "ORD-1")
.executionContext(TestExecutionContexts.tenantRun("office-a", "run-1"))
.step("accept", new AcceptOrderStep(orderService))
.step("payment", new WaitForPaymentStep())
.build();
harness.submit(flow)
.tick()
.assertFlow("order", "ORD-1")
.isRunning()
.currentStepIs("payment")
.tenantIdIs("office-a")
.runIdIs("run-1");
harness.publish(new PaymentApproved("ORD-1"))
.tick()
.assertFlow("order", "ORD-1")
.isFinished();For durable Flow recovery tests, reuse the same fake checkpoint store through
restart() and recover with a FlowFactoryRegistry:
FlowTestHarness restarted = harness.restart();
int recovered = restarted.recoverActiveCount(registry);
restarted.tick()
.assertFlow("order", "ORD-1")
.currentStepIs("payment")
.runIdIs("run-1");The first version intentionally avoids a large assertion DSL or a JUnit-only
API. Failed FlowAssertions checks throw AssertionError, so the helpers work
with JUnit, AssertJ, or plain test code.
Flower is being dogfooded against real application code, not only toy examples. It is currently being run and hardened in:
- architecture-office SaaS document workflows
- a Terminal Operating System execution layer
- game server workflow and turn/state coordination
- controlled AI automation through Flower Agent, Flower AI Harness, and Flower Action Runtime
These projects keep Flower honest about practical needs: explicit flow structure, small steps, recoverable execution, observable state, and code that stays understandable as systems grow. This is not a claim of broad external adoption; it is a statement about the real projects driving Flower's design.
When generating Flower code, prefer this pattern:
- Model business phases as explicit steps with stable ids.
- Keep each step small: start work, check state, emit result.
- Use
stay()for asynchronous waits anddone()only when the condition is definitely satisfied. - Do not create background threads inside steps unless the application service owns them.
- Do not block a worker tick. Flower gets its composability from quick, repeatable ticks.
- Use constructor-injected services and plain Java objects.
- Use events for cross-step or external completion signals.
- Write tests with
engine.attach()andworker.tickOnce()so behavior is deterministic. - Prefer readable flow builders over hidden reflection or annotation magic in core code.
That shape keeps Flower flows easy for humans and AI tools to inspect: the current step is visible, transitions are explicit, and waiting behavior is encoded as small repeatable decisions.
AI coding agents should treat Flower as a structure provider, not just another library call. The goal is not to generate more code faster. The goal is to make generated orchestration small enough that a human can read it, test it, and repair it later.
flower-check is the build-time enforcement tool for this direction. It can
fail a build when generated code uses known bad patterns, such as blocking a
worker tick or hiding orchestration outside Flower's Flow/Step boundary.
The Flower plugin makes the same rules available before code is written:
AI coding agent
-> uses the Flower plugin for the right pattern
-> generates Flow / Step code
-> flower-check verifies the result
-> tests prove the behavior
A future developer MCP may provide richer tool-driven access to Flower concepts, examples, and checks. It is an optional extension of the current loop: guidance before generation, explicit structure in code, checks during build, and deterministic tests for behavior.
To build and test the repository locally:
mvn -B verifyApplications should normally consume the released 0.1.3 artifacts from
Maven Central as shown in Install From Maven Central.
Contributors working on the separately built Gradle checker should follow the
additional local development steps in CONTRIBUTING.md.
Release preparation and Maven Central publisher setup are documented in docs/RELEASING.md.
Flower is licensed under the Apache License 2.0.
