# 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: 1. **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). 2. **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__rule` rows. ## 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 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 (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 `` with `initialValues` from 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 `` - If no formKey, shows a simple "Complete" button with no form ## 3. P3.5 — Rules Engine ### Rule definition schema (stored in `metadata__rule.payload`) ```typescript 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 }> 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): ```kotlin @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` via Jackson, then evaluates each condition: ```kotlin fun evaluateCondition(eventMap: Map, 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 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` 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.kt` - `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/rules/RuleEvaluator.kt` - `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/web/RuleController.kt` - `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/yaml/RuleYaml.kt` - `platform/platform-metadata/src/test/kotlin/org/vibeerp/platform/metadata/rules/RuleEvaluatorTest.kt` ### New files (Frontend) - `web/src/pages/UserTasksPage.tsx` - `web/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