# ADR-0015: Beoordeling escalation reassigns via an external-worker task - **Status:** Accepted - **Date:** 2026-07-17 - **Deciders:** Respellion engineering - **Relates to:** S-14 (#15); proposal #98. Builds on ADR-0009 (external-task worker / Workflow Client), ADR-0013 (behandel-portal wiring, the `Beoordelen` user task), ADR-0014 (the boundary-event pattern on `Beoordelen`). ## Context S-14 escalates a beoordeling that a behandelaar does not pick up in time: after 14 days the case must move to the `teamlead` role (PRD §5, flow 5). The `Beoordelen` user task already exists, claimable by the `behandelaar` candidate group; the teamlead role is seeded in the medewerker realm. Two forces shape this. 1. **The task must stay open.** Escalation changes *who may claim* an unclaimed beoordeling, not the work itself — so the timer must be **non-interrupting**: the `Beoordelen` task keeps running while escalation happens alongside it. 2. **Reassigning an open task's candidate group needs code.** Flowable cannot rewrite the candidate groups of an already-open user task from BPMN XML alone — that requires either a Java delegate/listener embedded in the engine, or an out-of-process actor driving the REST API. The repository has held a "stock Flowable image, no custom jars; the Workflow Client is the only code that talks to Flowable (§8.2)" posture since ADR-0009. ## Decision **A non-interrupting `P14D` boundary timer on `Beoordelen` fires an external-worker task (`BeoordelingEscaleren`); the Workflow Client reassigns the still-open `Beoordelen` task from the behandelaar group to teamlead.** - **Modelled in BPMN, driven by an external worker.** The timer routes a parallel token to an `external-worker` service task on the `BeoordelingEscaleren` topic, ending at a dedicated "Beoordeling geëscaleerd" end event. The model owns *when* escalation happens; the Workflow Client — the only code that talks to Flowable (§8.2) — owns *how* the reassignment is applied, exactly as `OpenZaakAanmaken` delegates the ZGW call (ADR-0009). No custom code runs inside Flowable. - **Reassignment is a candidate-group swap.** The escalation worker finds the still-open `Beoordelen` task in the escalating instance (task query by `processInstanceId` + `taskDefinitionKey`), adds `teamlead` as a candidate group via the task identity links, then removes `behandelaar`. The task now belongs to the teamlead; its history and variables are untouched. - **Best-effort, mirroring beoordeling and withdrawal.** If the task is no longer open — the behandelaar completed it in the window before the timer fired — the reassignment is a no-op. A failed reassignment leaves the escalation job un-completed so Flowable redelivers it (§8.6), consistent with the `OpenZaakAanmaken` worker. - **Segregated interface.** The escalation methods live on `IBeoordelingEscalatieClient`, separate from the `OpenZaakAanmaken` worker's `IExternalWorkerClient`, so the OpenZaak worker never sees escalation (interface segregation). Both are implemented by the one `FlowableWorkflowClient`. ## Consequences **Positive** - The escalation trigger is visible in `registratie.bpmn`; Flowable stays a stock image, and the Workflow Client remains the sole Flowable client (§8.2 upheld, not bent). - Reuses the external-worker mechanics (topic acquire/complete, hosted pump, per-tick scope, redelivery-on-failure) wholesale — the new code is one client capability, one processor, one pump. - Escalation latency is bounded by the worker's poll interval (seconds) — negligible against a 14-day timer. **Negative / costs** - Escalation is two REST hops (add teamlead, remove behandelaar) rather than one atomic update; between them the task is briefly claimable by both groups. Harmless at these volumes, and the pair is idempotent on redelivery. - The Flowable identity-link and management-job REST shapes are validated live (verify-domain fires the timer early via the management API), not in the Workflow Client's unit tests, which stub the HTTP exchange and assert only the request shape — consistent with ADR-0009 and ADR-0014. ## Alternatives considered - **Flowable timer/task listener (Java delegate).** Reassign in-engine when the timer fires. Rejected: it needs a custom jar in Flowable, breaking the stock-image, REST-only posture and adding a build/deploy surface to the engine for no capability the external-worker route lacks. - **Interrupting timer that re-creates the task for teamlead.** Cancel `Beoordelen` and start a fresh teamlead task. Rejected: it loses the task's identity/history and complicates correlation, where a candidate-group swap on the same task expresses "the same work, now the teamlead's" directly.