Developer Guide
This section covers the data model, configuration reference, and extension points available to developers integrating Process Manager into an application.
Data Model
Process Manager persists two entities: a process definition and its execution history. Each process holds a cron schedule, a reference to the Runnable bean that will be executed, and two independent status columns. Each execution record captures the start time, end time, and duration of a single run.
---
config:
layout: elk
---
erDiagram
pm_process {
integer id PK
varchar name
varchar schedule_status
varchar execution_status
varchar schedule
varchar job_qualified_name
}
pm_execution {
integer id PK
date start_date
time start_time
date end_date
time end_time
bigint duration
integer process_id FK
}
pm_process ||--o{ pm_execution : "has"
Database Schema
Process Manager manages two tables. The schema below represents the DDL generated by Hibernate for a standard relational database. Column names follow Hibernate's default snake_case naming strategy.
CREATE SEQUENCE pm_process_seq
START WITH 1
INCREMENT BY 50;
CREATE SEQUENCE pm_execution_seq
START WITH 1
INCREMENT BY 50;
CREATE TABLE pm_process (
id INTEGER NOT NULL,
name VARCHAR(255),
schedule_status VARCHAR(255),
execution_status VARCHAR(255),
schedule VARCHAR(255),
job_qualified_name VARCHAR(255),
CONSTRAINT pk_pm_process PRIMARY KEY (id)
);
CREATE TABLE pm_execution (
id INTEGER NOT NULL,
start_date DATE,
start_time TIME,
end_date DATE,
end_time TIME,
duration BIGINT,
process_id INTEGER NOT NULL,
CONSTRAINT pk_pm_execution PRIMARY KEY (id),
CONSTRAINT fk_pm_execution_process FOREIGN KEY (process_id) REFERENCES pm_process (id)
);
The schedule_status and execution_status columns store the string representation of the ScheduleStatus and ExecutionStatus enums respectively. The schedule column holds a standard six-field cron expression. The job_qualified_name column stores the fully qualified class name of the Runnable bean assigned to the process.
The start_date, start_time, end_date, and end_time columns of pm_execution are stored in UTC. Conversion to the time zone of the browser happens in the presentation layer.
The sequences are created by Hibernate when GenerationType.AUTO resolves to a sequence-based strategy, which is the default behaviour in Hibernate 7.
Module Overview
Process Manager is structured as six Maven modules following the AppJars layered architecture:
| Module | Artifact ID | Description |
|---|---|---|
| Model | appjars-process-manager-model |
DTOs, enums, filters, exceptions, auto-configuration |
| Business API | appjars-process-manager-business |
ProcessService and ProcessExecutionService interfaces |
| Business Impl | appjars-process-manager-business-impl |
Service implementations with scheduling logic |
| Data API | appjars-process-manager-data |
ProcessDao and ProcessExecutionDao interfaces |
| Data Impl | appjars-process-manager-data-impl |
JPA entities and DAO implementations |
| Flow UI | appjars-process-manager-flow |
Vaadin views, dialogs, and route configuration |
A monolithic application includes the three implementation modules (-business-impl, -data-impl, -flow). The API modules (-business, -data) are pulled in transitively.
Spring Auto-Configuration
Process Manager registers itself through Spring Boot's auto-configuration mechanism. The entry point is ProcessManagerAutoConfiguration, declared in:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
This class scans the com.appjars.processmanager package for all Spring components and JPA entities, so no additional @EntityScan or @ComponentScan declarations are required in the host application beyond those described in the Getting Started guide.
On application startup, the service listens to ContextRefreshedEvent to restore scheduled processes. It loads all persisted processes and normalises them:
- Any process whose
Runnablebean can no longer be resolved is transitioned toTASK_NOT_FOUND. - Any process left with a null schedule status is defaulted to
UNSCHEDULED. - Any process still marked
RUNNINGfrom a previous run — a state that cannot survive a restart — is reset toREADY. - A process that fails to normalise is logged and skipped, so a single bad row cannot abort startup.
Processes whose schedule status is ENABLED then have their cron triggers re-registered. Rescheduling is defensive: a process whose task cannot be resolved is set to TASK_NOT_FOUND, and one that fails for any other reason — including a free-tier limit that the persisted process count exceeds — is left PAUSED rather than dropped. Each case is logged with the process id.
On shutdown, a @PreDestroy hook resets stale RUNNING processes and cancels every registered trigger.
Scheduling
Process Manager owns the scheduler it runs jobs on. ProcessManagerSchedulerConfiguration declares a ThreadPoolTaskScheduler under the dedicated bean name processManagerTaskScheduler, built from Spring Boot's ThreadPoolTaskSchedulerBuilder, and ProcessServiceImpl injects it by name.
Two consequences matter for the host application:
@EnableSchedulingis not required. That annotation only registers the post-processor that drives@Scheduledmethods, which this appjar does not use. Owning the scheduler avoids activating scheduling globally as a side effect of adding the appjar.- No bean clash. Because the scheduler is injected by name, an application that declares its own
TaskScheduler— including one that does use@EnableSchedulingfor its own@Scheduledmethods — does not interfere with Process Manager.
The scheduler honours the standard spring.task.scheduling.* properties, and its threads carry the process-manager-scheduler- name prefix.
Configuration Properties
The following properties can be set in application.properties to customise the behaviour of Process Manager:
| Property | Default | Description |
|---|---|---|
com.appjars.processmanager.url.process |
pm/process |
URL path of the process management view |
com.appjars.processmanager.executionsdialog.datetimeformat |
dd/MM/yyyy HH:mm:ss.SS |
Pattern used for the Start and End columns of the execution history grid |
com.appjars.processmanager.executionsdialog.dateformat |
dd/MM/yyyy |
Pattern used for the date part of the execution history filters |
Process Status Lifecycle
A process carries two orthogonal statuses, persisted in separate columns and modelled by separate enums. The schedule status describes the state of the cron trigger; the execution status describes whether the task is running. Executing a process never overwrites its schedule status, so a process that is running still records the schedule it belongs to.
Schedule status
stateDiagram-v2
[*] --> PAUSED : save with schedule
[*] --> UNSCHEDULED : save without schedule
PAUSED --> ENABLED : resumeProcess()
ENABLED --> PAUSED : pauseProcess()
ENABLED --> UNSCHEDULED : disableProcess()
PAUSED --> UNSCHEDULED : disableProcess()
TASK_NOT_FOUND --> UNSCHEDULED : disableProcess()
ENABLED --> TASK_NOT_FOUND : task missing at startup
PAUSED --> TASK_NOT_FOUND : task missing at startup
| Status | Description |
|---|---|
ENABLED |
A cron trigger is registered and will fire |
PAUSED |
The schedule is preserved but its trigger is cancelled |
UNSCHEDULED |
No schedule is defined for the process |
TASK_NOT_FOUND |
The Runnable referenced by job_qualified_name no longer resolves |
resumeProcess() is valid only from ENABLED and PAUSED; any other status raises InvalidProcessStatusTransition. pauseProcess() is valid only from ENABLED. disableProcess() clears the cron expression and sets UNSCHEDULED regardless of the current status.
Execution status
stateDiagram-v2
[*] --> READY
READY --> RUNNING : execution starts
RUNNING --> READY : execution ends
| Status | Description |
|---|---|
RUNNING |
The process task is executing |
READY |
The process is not executing |
runProcess() raises InvalidProcessStatusTransition when the process is already RUNNING. Saving or updating a process resets its execution status to READY.
Real-Time Status Propagation
Both statuses reach the browser without polling. The business layer publishes a Spring application event on every change — ProcessScheduleStatusChangedEvent from ProcessServiceImpl, and ProcessExecutionStatusChangedEvent from ScheduledTaskPersistenceServiceImpl — carrying the process id and the new status. Neither class depends on the UI.
flowchart TD
A[Status changes in the business layer] --> B[Spring application event]
B --> C[ProcessStatusSignals]
C --> D[SharedValueSignal per process]
D --> E[Badges and row actions in every open ProcessListView]
ProcessStatusSignals is a singleton that listens for both events and holds one SharedValueSignal per process and per status kind. ProcessListView binds its badges and its pause, resume, and menu controls to those signals, so a change made by a background scheduler thread or by another administrator repaints every open view. No UI.access() call is involved.
Because propagation happens over server push, the host application must enable it with @Push on its AppShellConfigurator class. Without it, the badges only reflect the state at the moment the view was loaded.
Task Discovery
Process Manager discovers background tasks by scanning the Spring application context for beans that implement java.lang.Runnable. Each discovered bean is wrapped in an AssignableTask and made available for assignment when creating or editing a process.
To register a task, create a @Component that implements Runnable:
@Component
public class ReportGenerationTask implements Runnable {
@Override
public void run() {
// task logic
}
}
The class name displayed in the UI is derived from the bean's simple class name. The fully qualified class name is persisted in job_qualified_name so the scheduler can reconstruct the task after a restart.
Service API
ProcessService
ProcessService is the primary interface for managing process definitions. It extends CrudService and exposes the following methods specific to the module:
// Retrieve a paginated, filtered, and sorted stream of processes
Stream<ProcessDto> getProcesses(int offset, int limit, ProcessFilter filter, List<ProcessSort> sortOrder);
// Count processes matching a filter
Integer countProcesses(int offset, int limit, ProcessFilter filter);
// Return all Runnable beans available for assignment
Set<AssignableTask> getTasks() throws ClassNotFoundException;
// Register the cron trigger and transition the schedule status to ENABLED
void resumeProcess(ProcessDto process) throws ClassNotFoundException;
// Cancel the trigger and transition the schedule status to PAUSED
void pauseProcess(ProcessDto process);
// Clear the schedule and transition the schedule status to UNSCHEDULED
void disableProcess(ProcessDto process);
// Execute the process once immediately, regardless of its schedule
void runProcess(ProcessDto process) throws ClassNotFoundException;
// Persist a process after a run, without applying the scheduling side effects of update()
void updateOnRun(ProcessDto process);
resumeProcess(), pauseProcess(), and runProcess() throw InvalidProcessStatusTransition when called on a process in a state that does not allow the requested transition. getTasks(), resumeProcess(), and runProcess() throw ClassNotFoundException when the Runnable referenced by the process cannot be resolved in the application context.
In free mode the limits are enforced in the service layer, not only in the view. save() throws FreeLimitReachedException once the free-tier process count is reached, and update(), runProcess(), resumeProcess(), pauseProcess() and disableProcess() throw it when the persisted process count is above that limit — a state reachable only from an application that previously ran with a license. A host application calling these methods outside the bundled views must handle the exception.
ProcessFilter carries id, name, scheduleStatus, and executionStatus; the two status fields filter on the corresponding columns independently.
ScheduledTaskPersistenceService
Persisting the state of a running process is delegated to a separate service so that the transaction that records progress is not rolled back by a failure inside the task itself:
// Claim the process for execution if it is currently READY; returns whether the run may proceed
boolean tryStartExecution(ProcessDto process);
// Persist the process as RUNNING before the task is invoked
void updateProcessBeforeExecution(ProcessDto process);
// Persist the process as READY and store the execution record after the task returns
void saveExecutionAndUpdateProcess(ProcessDto process, ProcessExecutionDto execution);
tryStartExecution() is what makes concurrent runs safe: the bundled implementation performs an atomic conditional update, so only one caller can move a process out of READY. It carries a default implementation that delegates to updateProcessBeforeExecution() and returns true, which keeps a custom persistence implementation written against the earlier contract source compatible — such an implementation does not get the atomicity guarantee.
updateProcessBeforeExecution() and saveExecutionAndUpdateProcess() publish a ProcessExecutionStatusChangedEvent after committing.
ProcessExecutionService
ProcessExecutionService provides read access to the execution history:
// Retrieve a paginated, filtered, and sorted stream of execution records
Stream<ProcessExecutionDto> getProcessExecutions(int offset, int limit, ProcessExecutionFilter filter, List<ProcessExecutionSort> sortOrder);
// Count execution records matching a filter
Integer countProcessExecutions(int offset, int limit, ProcessExecutionFilter filter);
The bounds carried by ProcessExecutionFilter (startDate, startTime, endDate, endTime) are expressed in UTC, matching the stored columns. Each is optional and applied independently, so a filter may constrain a date without constraining a time.
Execution Flow
Every run — scheduled or triggered manually — is wrapped in a ScheduledTask and submitted to the appjar's own scheduler. It performs the following steps:
- The process is claimed for execution through
tryStartExecution(), which moves it toRUNNINGonly if it is currentlyREADY. The schedule status is left untouched. - The start instant is captured in UTC.
- The
Runnabletask'srun()method is invoked. - The end instant is captured in UTC and the execution status is set back to
READY. - The execution status and a new
pm_executionrow holding the start, end, and duration in milliseconds are persisted together.
If the claim in step 1 fails, the run is an overlap: a previous execution of the same process is still in progress. The task returns immediately, a warning naming the process id is logged, and no pm_execution row is written. A schedule shorter than the time the task takes to run therefore skips ticks instead of executing the task concurrently with itself.
If the task throws, the throwable is caught and logged, the execution record is still saved, and the execution status still returns to READY. A failing task therefore does not affect the scheduling of future runs.
Customisation
Assigning a Router Layout
By default, Process Manager registers its view without a parent layout. To wrap it in the application's main layout, inject the RouteConfigurer bean using its qualifier and call setViewsRouterLayout in a @PostConstruct method:
@Autowired
@Qualifier("ProcessManagerRouteConfigurer")
private RouteConfigurer routeConfigurer;
@PostConstruct
public void configure() {
routeConfigurer.setViewsRouterLayout(MainLayout.class);
}
The @Qualifier is required because multiple AppJars may contribute a bean named RouteConfigurer.
Customising the View URL
The default URL path can be overridden in application.properties:
com.appjars.processmanager.url.process=myapp/scheduled-tasks
Testing Against the Views
The process views expose stable data-testid attributes so that integration tests can select elements without depending on the DOM structure or on translated labels. The identifiers are declared as constants in com.appjars.processmanager.flow.util.TestIds and cover the grid, the filter fields and buttons, the row action controls, and both dialogs.
Selecting by these attributes is the supported way to drive the views from a test, and keeps tests unaffected by layout or wording changes.