Rules Engine + User-Task Form Rendering — Design Spec
Sub-projects B+C of the v1.0 remaining work. Covers P3.5 (rules engine) and P2.3 (user-task form rendering). P2.2 (BPMN designer) is deferred to v1.1.
1. Context
With P3.2/P3.3/P3.6/R3 shipped, the framework has metadata-driven forms, a form designer, and a metadata admin UI. Two pieces remain for v1.0:
P2.3 — User-task form rendering: Flowable user-tasks need a SPA page where operators can see pending tasks and complete them by filling out metadata-driven forms (using the MetadataFormRenderer we just built).
P3.5 — Rules engine: Key users need to create "if event X happens and condition Y is met, then do action Z" rules through the metadata admin UI, without writing code. Rules are stored as
metadata__rulerows.
2. P2.3 — User-Task Form Rendering
New backend endpoints
Add to WorkflowController:
GET /api/v1/workflow/tasks
→ List pending user-tasks for the current user (or all if admin)
Returns: List<UserTaskSummary> with taskId, taskName, formKey,
processDefinitionKey, processInstanceId, createTime, assignee
GET /api/v1/workflow/tasks/{taskId}
→ Get task details + variables
Returns: UserTaskDetail with taskId, taskName, formKey, variables,
processDefinitionKey, processInstanceId, createTime
POST /api/v1/workflow/tasks/{taskId}/complete
→ Complete the task with form values
Request body: Map<String, Any?> (the submitted form data)
Returns: 204 No Content
These use Flowable's TaskService.createTaskQuery() and
TaskService.complete(taskId, variables).
The formKey on the user-task (set in BPMN XML as flowable:formKey="vibe:plate-approval-task")
maps to a form definition slug in metadata__form. The SPA strips the vibe:
prefix and fetches the form via the existing
GET /api/v1/_meta/metadata/forms/{slug} endpoint.
New SPA page: UserTasksPage
Route: /workflow/tasks
- Lists pending user-tasks in a DataTable (task name, process, created time, assignee)
- Click a task → expands inline or navigates to
/workflow/tasks/{taskId} - TaskDetailPage renders the form using
<MetadataFormRenderer slug={formKey} />withinitialValuesfrom the task variables - Submit button calls
POST /api/v1/workflow/tasks/{taskId}/complete - On success, navigates back to the task list
SPA route: TaskDetailPage
Route: /workflow/tasks/:taskId
- Fetches task detail (variables + formKey)
- Strips
vibe:prefix from formKey → slug - Renders
<MetadataFormRenderer slug={slug} initialValues={variables} onSubmit={completeTask} /> - If no formKey, shows a simple "Complete" button with no form
3. P3.5 — Rules Engine
Rule definition schema (stored in metadata__rule.payload)
interface RuleDefinition {
slug: string // unique identifier
name: string // display name
description?: string
enabled: boolean // toggle on/off without deleting
triggerEvent: string // event class simple name: "SalesOrderConfirmedEvent"
conditions: Array<{
field: string // event property path: "totalAmount"
operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in'
value: string // compared value (coerced to field type at eval time)
}>
conditionLogic: 'AND' | 'OR' // how to combine multiple conditions
actions: Array<{
type: 'log' | 'set-field' | 'publish-event'
config: Record<string, string>
}>
version: number
}
Built-in action types (v1.0)
| Action type | What it does | Config |
|---|---|---|
log |
Writes an INFO log line with a template message | { message: "Order {orderCode} exceeded threshold" } |
set-field |
Sets a field on the event's aggregate via a REST call or ext update | { entity: "SalesOrder", field: "ext.priority", value: "HIGH" } |
publish-event |
Publishes a synthetic event to the event bus | { eventType: "RuleTriggeredEvent", data: "{...}" } |
v1.0 keeps actions simple. Complex actions (send email, call webhook, create entity) are v1.1.
Backend: RuleEngine service
New class in platform-metadata (or a new platform-rules module — but YAGNI,
put it in platform-metadata since rules are metadata):
@Component
class RuleEngine(
private val eventBus: EventBus,
private val jdbc: NamedParameterJdbcTemplate,
private val objectMapper: ObjectMapper,
) {
@PostConstruct
fun loadAndSubscribe() {
// 1. Read all enabled rules from metadata__rule
// 2. Group by triggerEvent
// 3. Subscribe to each distinct event type via eventBus.subscribe(topic, listener)
// using the aggregateType as the topic string
// 4. When an event fires, evaluate conditions against event properties
// 5. If conditions match, execute actions
}
fun refresh() {
// Unsubscribe all, reload, resubscribe
// Called after rule CRUD operations
}
}
Condition evaluation
Events are domain objects with typed properties. The rule engine serializes the
event to a Map<String, Any?> via Jackson, then evaluates each condition:
fun evaluateCondition(eventMap: Map<String, Any?>, condition: Condition): Boolean {
val actual = eventMap[condition.field] ?: return false
val expected = condition.value
return when (condition.operator) {
"eq" -> actual.toString() == expected
"neq" -> actual.toString() != expected
"gt" -> (actual as? Number)?.toDouble()?.let { it > expected.toDouble() } ?: false
"gte" -> (actual as? Number)?.toDouble()?.let { it >= expected.toDouble() } ?: false
"lt" -> (actual as? Number)?.toDouble()?.let { it < expected.toDouble() } ?: false
"lte" -> (actual as? Number)?.toDouble()?.let { it <= expected.toDouble() } ?: false
"contains" -> actual.toString().contains(expected)
"in" -> expected.split(",").map { it.trim() }.contains(actual.toString())
else -> false
}
}
Backend endpoints (in MetadataController or new RuleController)
GET /api/v1/_meta/metadata/rules → List<RuleDefinition>
GET /api/v1/_meta/metadata/rules/{slug} → RuleDefinition
PUT /api/v1/_meta/metadata/rules/{slug} → RuleDefinition (upsert, source='user')
DELETE /api/v1/_meta/metadata/rules/{slug} → 204
Same source='user' enforcement as forms/list-views.
After every write, call ruleEngine.refresh().
SPA: Rules tab in MetadataAdmin
Add a 7th tab "Rules" to MetadataAdminPage:
- DataTable showing: Name, Trigger Event, Enabled (toggle), Conditions (count), Actions (count), Source badge
- "New Rule" button opens an inline form:
- Name, description
- Trigger event dropdown (populated from known event types)
- Conditions builder: field, operator, value (add/remove rows)
- Condition logic toggle (AND/OR)
- Actions builder: type dropdown, config key-value inputs (add/remove rows)
- Enabled checkbox
- Save/Cancel
- Edit button per source='user' row
- Delete button per source='user' row
MetadataYaml extension
Add rules: List<RuleYaml> to MetadataYamlFile so core PBCs and plug-ins
can ship default rules in their metadata YAML. Same loader/wipe pattern.
4. Scope Decisions
| Decision | Choice |
|---|---|
| BPMN designer (P2.2) | Deferred to v1.1 — customers use Camunda Modeler or hand-edit BPMN XML |
| Rule action types | v1.0: log, set-field, publish-event. v1.1: email, webhook, create entity |
| Rule condition complexity | v1.0: flat field conditions with AND/OR. v1.1: nested groups, expressions |
| User-task assignment | v1.0: all tasks visible to all authenticated users. v1.1: role-based assignment |
5. File Inventory
New files (Backend)
platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/rules/RuleEngine.ktplatform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/rules/RuleEvaluator.ktplatform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/web/RuleController.ktplatform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/yaml/RuleYaml.ktplatform/platform-metadata/src/test/kotlin/org/vibeerp/platform/metadata/rules/RuleEvaluatorTest.kt
New files (Frontend)
web/src/pages/UserTasksPage.tsxweb/src/pages/TaskDetailPage.tsx
Modified files
-
platform/platform-workflow/src/main/kotlin/org/vibeerp/platform/workflow/http/WorkflowController.kt— add task endpoints -
platform/platform-workflow/src/main/kotlin/org/vibeerp/platform/workflow/WorkflowService.kt— add task methods -
platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/yaml/MetadataYaml.kt— add RuleYaml -
platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/MetadataLoader.kt— process rules -
web/src/pages/MetadataAdminPage.tsx— add Rules tab -
web/src/api/client.ts— add workflow task + rule API functions -
web/src/types/api.ts— add UserTask + Rule types -
web/src/i18n/messages.ts— add keys -
web/src/App.tsx— add routes -
web/src/layout/AppLayout.tsx— add nav entry -
platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/SpaController.kt— add workflow routes
6. Testing
-
RuleEvaluatorTest.kt— unit tests for condition evaluation (eq, gt, lt, contains, in, AND/OR logic) -
RuleEngine— integration: load rules from metadata, verify event subscription and action execution - Manual smoke: create a rule via UI, trigger the event, verify action fires
- Manual smoke: start a BPMN process with a user-task, see it in the tasks page, complete it with the form