Dynamic, Extensible, and Developer-Friendly YAML-based Task Orchestrator
Orchid is a lightweight yet powerful job orchestrator built with Java 17 and Spring Boot. It empowers you to define complex workflows using intuitive YAML configuration files, where each task (job) is broken down into sequential stages, and each stage into a series of actions. Orchid's core philosophy is to provide flexibility and extensibility, allowing for custom Java classes (plugins) to be loaded per job without recompiling the orchestrator.
This project aims to simplify the automation of sequential, conditional, and data-driven processes by offering a declarative approach to workflow management.
- Declarative YAML Workflows: Define jobs, stages, and actions in a human-readable YAML format.
- Hierarchical Structure: Organize tasks logically:
Jobs>Stages>Actions. - Versatile Action Types:
spel: Execute SpEL (Spring Expression Language) expressions for dynamic logic, data manipulation, and assignments.loop: Iterate over numerical ranges or collections from thejobContext. Supports modification of thejobContextfrom within the loop body.conditional: Execute a sequence of actions based on the boolean outcome of a SpEL expression.command: Run operating system commands.javaMethod: Highly flexible action to:- Instantiate classes (from plugins or classpath) using constructors (with or without arguments).
- Invoke methods on existing objects in the
jobContext. - Invoke methods on Spring-managed beans.
- Invoke methods on newly instantiated objects.
- Dynamic Job Context (
jobContext):- A
ConcurrentHashMapshared across all actions within a single job execution. - Stores initial parameters, intermediate results, and final outputs.
- Supports nested parameters from
parameters.ymlwhich are automatically flattened (e.g.,http.delaybecomes accessible asjobContext['http.delay']).
- A
- Implicit Previous Result (
#previousResult):- Within a sequence of actions (e.g., under a stage or in a loop/conditional body), the special SpEL variable
#previousResultholds the unboxed result of the immediately preceding action in that sequence.
- Within a sequence of actions (e.g., under a stage or in a loop/conditional body), the special SpEL variable
- Flexible Result Handling (
returnToContextAs):- Assigns the result of an action (after automatic
Optionalunboxing) to a key injobContext. - Can be a simple key name (e.g.,
myResultKey). - Can be a SpEL expression for more complex assignments or updates to existing objects in
jobContext(e.g.,#myObject.setProperty(#actionResult)), where#actionResultis the unboxed result of the current action.
- Assigns the result of an action (after automatic
- Per-Job Plugin System:
- Dynamically load
.jarfiles and individual.classfiles from alib/subdirectory within each job's folder. - Loaded classes are available to SpEL's
T()type operator (e.g.,T(com.myplugin.MyUtil).staticMethod()) and for instantiation via thejavaMethodaction.
- Dynamically load
- Automatic Job Loading & Selective Execution:
- Scans a
jobs/directory in the classpath at startup. - Executes all found jobs by default.
- Supports selective execution of jobs via the
--jobs=jobId1,jobId2command-line argument (or--jobs=all).
- Scans a
- Initial Parameters per Job:
- Define a
parameters.ymlalongsidejob.ymlfor initialjobContextvalues. - Supports nested structures which are flattened into dot-separated keys.
- Define a
- Internationalized Logging (i18n):
- Log messages are externalized (e.g.,
logs.properties,logs_es.properties). - Application locale for logs is configurable via
app.localeinapplication.properties.
- Log messages are externalized (e.g.,
- Detailed Execution Lineage Tracking: Logs clearly indicate the execution path (Stage > Parent Action > Current Action), aiding debugging.
- Argument Coercion & Instantiation for
javaMethod:- Automatic type coercion for common types (e.g.,
IntegertoLong) for constructor and method arguments. - Supports instantiating complex method parameter objects if the YAML argument is a SpEL list representing constructor arguments for that parameter type (e.g.,
args: ["#{ {arg1ForParamObject, arg2ForParamObject} }"]).
- Automatic type coercion for common types (e.g.,
Expected directory structure within src/main/resources/:
src/main/resources/
├── jobs/
│ └── {yourJobName}/
│ ├── job.yml # Job definition
│ ├── parameters.yml # (Optional) Initial parameters for this job
│ └── lib/ # (Optional) Directory for job-specific plugins
│ ├── com/example/MyUtil.class # Correct package structure for .class files
│ └── my-custom-lib.jar
│ └── ... (other jobs) ...
├── logs.properties # Default log messages (e.g., English)
├── logs_es.properties # Spanish log messages (or other locales)
└── application.properties # Spring Boot application configuration
Each job.yml defines a single job.
Example:
id: "ComplexDataProcessing"
description: "A job demonstrating various Orchid features"
initialContextParameters: ["inputPath", "config.retryAttempts"]
stages:
- name: "Initialization"
actions:
- name: "SetupInitialDirs"
type: "command"
command: "mkdir"
args: ["-p", "#jobContext['inputPath'] + '/output'"]
returnToContextAs: "outputDir"
- name: "CreateConfigObject" # Using javaMethod for instantiation
type: "javaMethod"
beanName: "com.example.jobplugins.MyJobConfig" # FQCN of a class in lib/
constructorArgs:
- "#jobContext['config.retryAttempts']" # e.g., an Integer
- "DEFAULT_MODE"
# No 'method' means the instance itself is the result
returnToContextAs: "jobConfigInstance"
- name: "DataTransformation"
actions:
- name: "LoadData"
type: "javaMethod"
beanName: "jobConfigInstance" # Using the instance from context
method: "loadDataFromFile"
args: ["#jobContext['inputPath'] + '/input.csv'"]
returnToContextAs: "loadedData" # Might be a List or custom object
- name: "TransformDataLoop"
type: "loop"
collection: "#loadedData" # Uses result of previous action
iteratorVariable: "currentItem"
body:
- name: "ProcessItem"
type: "spel"
expression: "#currentItem.toUpperCase() + '_processed'"
returnToContextAs: "processedItem" # This will be overwritten each iteration
# but #previousResult in next action will see it
- name: "LogProcessedItem"
type: "spel"
expression: "T(org.slf4j.LoggerFactory).getLogger('JobLogger').info('Processed: ' + #previousResult)"
# No returnToContextAs needed for logging
- name: "Finalization"
type: "javaMethod" # Stage as a single action
beanName: "com.example.jobplugins.ReportingUtil" # Another plugin class
method: "generateReport"
args: ["#jobContext"] # Pass the whole context
returnToContextAs: "reportStatus"Key Fields: (Refer to previous README versions for basic field descriptions. Below are highlights of recent additions/clarifications)
Action(Base):returnToContextAs:- Simple Key:
myResult- Stores the action's result (unboxed fromOptionalif applicable) intojobContext['myResult']. - SpEL Expression:
"#myObjectInContext.setSomeProperty(#actionResult)"- Executes the SpEL.#actionResultis the unboxed result of the current action.#previousResultis also available if it's not the first action in a sequence.
- Simple Key:
JavaMethodAction:beanName: Can be:- A key for an object already in
jobContext. - The name of a Spring-managed bean.
- A Fully Qualified Class Name (FQCN) of a class from a plugin (or classpath).
- A key for an object already in
constructorArgs: (Optional) A list of SpEL expressions evaluated to become arguments for the constructor whenbeanNameis a FQCN and the action is instantiating it.method: (Optional) The name of the method to invoke. If omitted (andbeanNameis a FQCN), the action's result is the newly instantiated object.args: A list of SpEL expressions for the method's arguments. If an argument is itself a complex object, you can provide a SpEL list literal#{ {...} }whose elements will be used to construct that parameter object.
Supports nested structures, which are flattened. E.g.:
server:
host: "localhost"
port: 8080
database:
url: "jdbc:..."Becomes accessible in jobContext as jobContext['server.host'], jobContext['database.url'], etc.
Place in jobs/{yourJobName}/lib/.
.classfiles: Must follow package structure (e.g.,lib/com/example/MyUtil.class)..jarfiles: Placed directly inlib/.
Run specific jobs from the command line:
java -jar orchid.jar --jobs=jobId1,anotherJobId
Use --jobs=all or omit --jobs to run all discovered jobs.
Build and run the Spring Boot application. The JobAutoLoaderRunner will handle job discovery and execution based on command-line arguments or defaults.
- Java 17
- Spring Boot 3.x
- Lombok
- Jackson (for YAML)
- SLF4J with Logback
Refer to build.gradle for details.
(Refer to the "Orchid Next Steps (English, Commented)" document for a detailed list of future ideas.)
Thank you for using Orchid!