Brook is a Java workflow orchestration engine for microservice calls and
in-process application logic. It can run embedded with brook-engine, or as a
Spring Boot integration through brook-spring-boot-starter. This manual covers
both approaches and the SPI-based extensions used in production deployments.
| Module | Purpose |
|---|---|
brook-common |
Shared models, configuration, utilities, and SPI loading infrastructure. |
brook-spi |
Public contracts for tasks, metadata, queues, execution storage, locks, callbacks, listeners, and computing engines. |
brook-core |
Flow execution, built-in control-flow tasks, local implementations, and flow-definition loading. |
brook-engine |
Convenience artifact for embedded applications. |
brook-spring-boot |
Spring Boot starter, auto-configuration, Spring task adapters, and optional Spring integrations. |
brook-spi-extensions |
Optional JavaScript computing, HTTP task, HTTP metadata, and Redis lock extensions. |
brook-demo |
A runnable Spring Boot application and flow-definition examples. |
Brook’s built-in task types are COMPUTING, IF, SWITCH, LOOP, WAIT,
and SUB_FLOW. Additional task types are supplied by dependencies or your
application, such as the optional HTTP task and Spring task adapters.
Build the repository with JDK 8 or later and Maven:
mvn clean verify
Use the version published to Maven Central.
<properties>
<brook.version>...</brook.version>
</properties>
Use the engine artifact when the application creates and wires Brook without Spring:
<dependency>
<groupId>xyz.mytang0.brook</groupId>
<artifactId>brook-engine</artifactId>
<version>${brook.version}</version>
</dependency>
Instantiate a FlowTaskRegistry and FlowExecutor, register the task
implementations needed by the application, then start a StartFlowReq. The
engine resolves its configuration and SPI implementations through Brook’s
configuration and extension facilities.
For Spring Boot, add the starter. It auto-configures FlowExecutor,
FlowTaskRegistry, FlowLogService, Spring configuration lookup, and the
Spring task adapters.
<dependency>
<groupId>xyz.mytang0.brook</groupId>
<artifactId>brook-spring-boot-starter</artifactId>
<version>${brook.version}</version>
</dependency>
Brook is enabled by default. Set brook.enabled=false to disable its Spring
auto-configuration.
The demo is in brook-demo/brook-demo-spring. It packages JSON definitions
from src/main/resources/META-INF/flows and exposes management endpoints under
/flow.
mvn -pl brook-demo/brook-demo-spring -am spring-boot:run
Start a background flow named test-simple:
curl -X POST http://localhost:8080/flow/instance/start \
-H 'Content-Type: application/json' \
-d '{"name":"test-simple","input":{"value":"example"}}'
The response is the flow ID. Fetch its latest state:
curl 'http://localhost:8080/flow/instance/get?flowId=<flow-id>'
The demo contains definitions for simple computation, lists and maps,
conditionals, switching, loops, HTTP calls, Spring bean methods, delays,
retries, timeouts, skips, and hanging asynchronous work. HTTP-related examples
target demo endpoints on localhost:8080; run the demo before invoking them.
The default file metadata service scans every classpath
META-INF/flows/**/*.json resource. The filename, without .json, is the
default lookup name, and each definition also declares its own name. File
definitions can be saved or updated only in the in-memory cache for the running
process; use a metadata extension when definitions must be durable or mutable.
A flow requires name and at least one entry in taskDefs. A task requires
type and name.
{
"name": "hello-flow",
"description": "A minimal flow",
"input": {
"name": "${input.name}"
},
"taskDefs": [
{
"type": "COMPUTING",
"name": "message",
"input": {
"source": "Hello, ${flow.input.name}!"
}
}
],
"output": "${message.output}"
}
version identifies a version when the selected metadata service supports
versions. failureFlowName identifies a compensating flow. A flow
controlDef supports timeoutMs, timeoutPolicy (TIME_OUT or
ALERT_ONLY), enableLog, concurrencyLimit, executionProtocol, and
queueProtocol.
Flow and task input and output values may be an object, array, or string.
Brook recursively replaces ${...} expressions using JSONPath against the
execution context. Common paths are:
| Expression | Meaning |
|---|---|
${flow.input} |
The mapped flow input. |
${flow.input.customerId} |
A field in the flow input. |
${taskName.output} |
Output from a completed task. |
${flow.extension} |
The extension data supplied when the flow was started. |
When an entire string is one expression, its underlying value retains its
type. When text surrounds one or more expressions, Brook produces a string.
Use $${ to render a literal ${. Missing JSONPath values resolve to null
when used alone and to an empty string when combined with other text.
Each task can contain a controlDef:
{
"startDelayMs": 3000,
"timeoutMs": 10000,
"timeoutPolicy": "RETRY",
"retryCount": 3,
"retryDelayMs": 1000,
"retryLogic": "EXPONENTIAL_BACKOFF",
"enableCache": true,
"concurrencyLimit": 10,
"skipDef": {
"engineType": "javascript",
"skipCondition": "$.flow.input.dryRun == true"
}
}
startDelayMs, timeoutMs, and retryDelayMs are milliseconds and must be
non-negative. timeoutPolicy can be TIME_OUT, ALERT_ONLY, or RETRY;
retry logic is FIXED or EXPONENTIAL_BACKOFF. Use checkDef.successDef to
evaluate business success and checkDef.retryDef to decide whether a
successful invocation should instead retry. Both conditions use the configured
computing engine.
hangDef models an asynchronous operation. The main task starts the work,
then determineTaskDef is executed repeatedly until it completes. For example,
the demo’s HTTP poller retries while its response body is "fail":
{
"hangDef": {
"determineTaskDef": {
"type": "HTTP",
"name": "poll-status",
"input": {
"uri": "http://localhost:8080/hang/test/success",
"method": "GET",
"params": { "id": "${output.body}" }
},
"checkDef": {
"retryDef": { "retryCondition": "$.body == 'fail'" }
},
"controlDef": { "retryCount": 100, "retryDelayMs": 5000 }
},
"feedbackOutput": true
}
}
Task definitions also support logDef (startFormat, terminalFormat),
progressDef (progress, interval), linkDef (title, url), and
callback (async, protocol, input). Use extension for task-specific
string options.
COMPUTING evaluates input.source with the selected engineType. The
JavaScript engine accepts expressions such as $.result; test-simple.json
shows both literal and JavaScript computation.
IF selects the first matching branch. Its input contains an engineType and
branches, each with condition and cases:
{
"type": "IF",
"name": "environment",
"input": {
"engineType": "javascript",
"branches": [{
"condition": "$.flow.input.envCode == 'dev'",
"cases": [{
"type": "COMPUTING",
"name": "devLogic",
"input": { "source": "dev" }
}]
}]
}
}
SWITCH uses input.case to select an array from
input.decisionCases; input.defaultCaseKey is selected when no key matches.
test-switch.json includes nested IF tasks.
LOOP accepts input.loopOver and an array in input.loopBody. Each loop
body task receives a unique __LOOP_<index> suffix. Within the loop body,
references to the original task name resolve to the latest loop iteration, as
shown in test-loop.json.
WAIT and SUB_FLOW are built-in task types for waiting and starting a nested
flow. Inspect their FlowTask option catalogs when defining their inputs, and
add a focused test definition alongside the application flow that uses them.
Add the HTTP extension for HTTP tasks:
<dependency>
<groupId>xyz.mytang0.brook</groupId>
<artifactId>brook-task-http</artifactId>
<version>${brook.version}</version>
</dependency>
An HTTP task requires uri and method; it also accepts body, headers,
params, variables, and charset. test-http.json calls the demo metadata
endpoint and maps its flow name into the query parameters.
Spring applications can call arbitrary beans with the springBeanMethod task:
{
"type": "springBeanMethod",
"name": "firstCall",
"input": {
"beanName": "demoService",
"methodName": "hello",
"args": ["${flow.input}"]
}
}
Provide types when overload resolution requires explicit Java class names.
The test-spring.json example demonstrates both forms.
FlowExecutor exposes two start modes:
startFlow(StartFlowReq): persists the instance and schedules it
asynchronously; returns its flow ID.requestFlow(StartFlowReq): waits for completion and returns the resulting
FlowInstance. The flow-level timeout bounds the wait when configured.Supply a named definition with {"name":"test-simple","input":{...}}, or put
an inline flowDef in StartFlowReq. extension may carry application
metadata that mapping expressions can read.
The demo maps the following executor methods to HTTP endpoints:
| Operation | Endpoint |
|---|---|
| Asynchronous start | POST /flow/instance/start |
| Synchronous request | POST /flow/instance/request |
| Submit an external task result | POST /flow/instance/execute |
| Skip a task | POST /flow/instance/skip |
| Trigger scheduling/decision | PUT /flow/instance/decide?flowId=... |
| Pause / resume | PUT /flow/instance/pause?flowId=..., PUT /flow/instance/resume?flowId=... |
| Terminate | PUT /flow/instance/terminate?flowId=...&reason=... |
| Read an instance | GET /flow/instance/get?flowId=... |
The demo also provides metadata management at /flow/metadata: POST saves,
PUT updates, GET reads by flowName and optional flowVersion, and
DELETE removes a definition. The file metadata service does not support
deletion.
Spring Boot reads Brook settings from the application environment. The core protocol selectors are:
brook.enabled=true
brook.metadata.protocol=
brook.execution-dao.protocol=
brook.queue.protocol=local
brook.lock.protocol=
An empty metadata protocol uses classpath file definitions. The local queue and local execution DAO are process-local defaults; select compatible SPI implementations for shared infrastructure.
| Capability | Artifact | Relevant settings |
|---|---|---|
| JavaScript computing | brook-computing-javascript |
Select engineType: "javascript" in flow tasks. |
| HTTP task | brook-task-http |
brook.task.http.clientConfig.connectionRequestTimeout, brook.task.http.clientConfig.socketTimeout, brook.task.http.clientConfig.connectTimeout, brook.task.http.clientConfig.maxConnTotal, and brook.task.http.clientConfig.maxConnPerRoute. |
| HTTP metadata | brook-metadata-http or Spring brook-spring-boot-http-metadata |
Set brook.metadata.protocol=http and brook.metadata.http.serverUri; optional keys include nameKey, versionKey, wrapped, cache settings. |
| MySQL metadata, queue, and execution DAO | brook-spring-boot-mybatis |
Enable each adapter with brook.metadata.mysql.enabled=true, brook.queue.mysql.enabled=true, and/or brook.execution-dao.mysql.enabled=true; choose the corresponding protocol. |
| Redis lock | brook-lock-redis or Spring brook-spring-boot-lock-redis |
Set brook.lock.protocol=redis and configure brook.lock.redis.config. |
The HTTP client defaults to 6-second connection-request, 10-second connect, and 30-second socket timeouts. Tune connection limits and timeouts for the service being called. Do not place credentials in flow JSON; source them from the deployment environment and map them only where needed.
Annotate Spring component methods with @Taskable to register application
task types:
@Component
public class InvoiceTasks {
@Taskable(type = "issue-invoice", description = "Create an invoice")
public String issue(String customerId) {
return customerId;
}
}
@Taskable also declares whether the method is asynchronous or retryable.
Use a flow task with type: "issue-invoice" and provide an input compatible
with the method parameters. For fully dynamic bean calls, use
springBeanMethod as described above.
Implement FlowTask for a new task type, including its required and optional
configuration options, and register it through Brook’s extension mechanism.
The same SPI approach applies to MetadataService, ExecutionDAO,
QueueService, LockService, TaskCallback, FlowListener, TaskListener,
Engine, and EngineActuator. Select named implementations with their
protocol settings or task/engine type.
Use task listeners for task lifecycle events, flow listeners for flow lifecycle
events, and callbacks when an external system must be notified of task
completion. Implement a computing Engine when a flow’s conditions or
expressions need a language other than the available engines.
LockService such as Redis
locking so competing workers do not execute the same flow concurrently.Contributions, bug reports, documentation improvements, and feature requests are welcome in GitHub Issues.
Validate the complete Maven build:
mvn clean verify