Skip to content

Process Designer

openwcs-docs-agent edited this page Jun 17, 2026 · 23 revisions

Process Designer

The process designer lets a non-developer build a configurable handheld operator process (goods-in, packing, stock check, an ad-hoc move) as a versioned definition, assign one version as active for a process type, and have the handheld PWA run it. It complements, and does not replace, the Flowable BPMN process-engine, which stays for heavy backend orchestration.

It is backed by the process-designer service (Java, port 8097, own process_designer schema), introduced in Phase 1, extended in Phase 2 (version management, JSON import/export, step skip conditions, more curated tasks, instance monitoring) and completed in Phase 3 (a sandboxed scripting escape hatch and design-time AI task-assist). A later increment added scan verification (the verify block on text/number input screens — see Scan verification below). The feature is now complete (spec: docs/process-designer-spec.md in the repo). See Services for the one-line summary.

Base path note. The process designer lives under /api/process-designer/**, deliberately not /api/process/** (the Flowable process-engine owns that prefix). The two engines coexist.

What a process is

A process is a versioned JSON flow of three kinds of steps:

  • Screens: what the operator sees. Six screen types ship: text, number, date, acknowledge, yes/no, choice. Each can validate input, bind to a scanner, and write its result into a named field on the process data object.
  • Tasks: calls into the rest of the WCS (see the curated library below). A task reads from the data object, runs, and merges named outputs back.
  • Compute (no-code, client-evaluated): an ordered set of { var, expr } rows the handheld evaluates in sequence against the data object, writing each result into the named variable before following next/transitions. No screen is rendered; no server call is made; no checkpoint is created. Use compute steps to derive flags and values (e.g. match = qty == expected) for downstream branching.

Steps connect with transitions; branch conditions decide the next step from the data collected so far. A step can also carry a skip condition (skipWhen, Phase 2): when it evaluates true, the runtime skips that step (the same restricted grammar as a transition condition). Compute steps are processed like skip-resolution — the handheld evaluates their set rows and advances without rendering a screen, so transitions/next see the freshly computed values. A data object carries values across the whole run, and screen titles / prompts can interpolate {{placeholder}} values from it (placeholders resolve to codes via the catalog, never raw UUIDs).

How to design a process

  1. Open Engineering → Process Designer (/process-design, ADMIN / SUPERVISOR). The permission is PROCESS_DESIGN_EDIT.
  2. The designer opens to a definitions table: one row per process key, showing its title, key, status, active version number, and total version count. Click a row to open that process in the editor; click + New process to start a blank draft. A ← Processes back button in the editor toolbar returns to this table (with an unsaved-changes confirmation prompt if needed).
  3. The editor is a WYSIWYG layout with a flow pane, a live handheld preview, a properties panel, and a version-management pane. The flow pane has a Canvas / List toggle at the top:
    • Canvas (default) — a zoomable visual node-canvas (powered by React Flow). Each step appears as a draggable node showing the step ID, type icon, step-type description, and status badges (start, verify, task, script, compute, or orphan). Nodes are draggable even on read-only (ACTIVE/ARCHIVED) versions — node position is pure layout and non-behavioural; the position is persisted only when the version is an editable draft. Drag from a node's right-hand connector to another node to create a link; the first link out of a node becomes its next, any further link becomes a new branch transition. Click a dashed branch edge's label to edit the when condition inline (unknown variables and syntax errors are flagged immediately). Loop-back edges (a step whose link points to an ancestor) are rendered as animated amber arrows. Orphan nodes (not reachable from start) have a dashed border and a warning badge. Node positions are auto-laid out on first open (BFS left-to-right, tighter spacing) and persisted in the definition so the layout survives reloads. Select a node to drive the properties panel and the live preview exactly as clicking in the list view; hover or select a node to reveal Set as start and Delete action buttons. A mini-map in the canvas corner aids navigation. In canvas mode the layout shifts to a wide canvas column on the left with the preview and properties panel stacked on the right.
    • List — the previous structured flow list, ordered by the real execution graph (DFS from the start step, following branches then next). Each step shows its outgoing edges as clickable links. A loop banner appears at the top of the list whenever a back-edge is detected, and the offending edge is badged ↺ loop. Steps not reachable from the start are grouped under "Not connected to the flow" with a warning indicator. Step order is managed by editing edges and using "Set as start" — there are no manual reorder arrows.
    • A "Data object" button at the bottom of the flow pane (labelled { } Data object (N)) opens the data-object dialog in both views.
    • The toolbar groups the process identity (title, key, icon, status) on the left and a compact action cluster on the right: Validate, Simulate, Save, and Publish are primary buttons; Duplicate, Export, and Import are tucked into a More ▾ overflow menu to keep the bar uncluttered.
    • a LIVE handheld preview that renders the selected screen exactly as the operator will see it (it reuses the very same ProcessScreenView component the runtime uses, so what you see is what runs),
    • a properties panel for the selected step (the data-object editor lives in its own dialog, see above),
    • a version-management pane (Phase 2) listing drafts / active / archived versions (only a DRAFT is editable), with duplicate / clone and JSON export / import controls.
  4. Add screens, tasks, and compute steps, set transitions, branch conditions and step skip conditions ("Skip this step when…"), and bind screens to the data object. The task picker is driven by the live server task catalog (GET /api/process-designer/tasks) and is a searchable combobox — type to filter across task label, type ID, and description. Each task in the catalog carries a one-sentence description of what it does and when to use it; each input and output carries a hint explaining what to map into it or what the value means. Both are shown in the guided task configuration dialog. Task and compute steps show an "Edit task…" button on the mockup card when selected; clicking it opens the guided task configuration dialog for mapping inputs and outputs (analogous to the Verify dialog). For a compute step, the dialog shows an ordered table of variable = expression rows with inline syntax validation; it also flags rows whose target variable is not declared in the data object or whose expression references an undeclared variable, and the Done button is disabled until every row is valid (declared target + a syntactically correct expression over declared variables only). Branch condition (when) fields and the "Skip this step when…" (skipWhen) field carry the same live check: if the expression is syntactically valid but references an undeclared variable, an inline warning appears immediately (⚠ Unknown variable(s): …) prompting the designer to add the variable in the data-object dialog before publishing.
  5. Use Simulate to walk the flow in the preview without touching real data.
  6. Validate, then Publish.

Version management, import and export (Phase 2)

  • Duplicate / clone: POST /defs/{key}/{version}/duplicate copies any version into a new DRAFT, so you can iterate from a published version without disturbing it.
  • Export: GET /defs/{key}/{version} returns the full definition JSON; the designer offers it as a download.
  • Import: POST /defs/import creates a new DRAFT from a full definition JSON (an upload in the designer). Together, export + import move a process between environments (dev → test → prod).

Drafts, active versions, and publishing

  • Every edit happens on a DRAFT version. Editing a non-draft is rejected.
  • Publish first validates the definition (step reachability, no dangling transitions, every writeTo / task reference resolves, a valid start, and Phase 2: no unreachable steps, no skippable step without an onward path, well-formed branch and skipWhen conditions — validated by a recursive-descent ConditionParser with no eval, so a malformed condition is rejected with 422 — and every identifier referenced in a transition when or skipWhen condition must be a declared data-object variable (ExpressionParser.identifiers cross-checks; null is treated as a literal and never flagged; 422 if any reference is undeclared) — task steps that supply every required input per the live catalog, no duplicate step ids, and compute steps whose set is non-empty with every var declared in the data schema and every expr valid per the expression grammar with all referenced variables also declared in the data schemaExpressionParser validates syntax and ExpressionParser.identifiers cross-checks variable references server-side, 422 on either failure). If anything is wrong you get the list of problems back and nothing changes.
  • On success, publish flips the draft to ACTIVE and archives the prior active version atomically (there is always exactly one ACTIVE version per process key). Versions auto-increment per key.
  • Instances that are already running are pinned to the definition they started on, so publishing a new version never disrupts an in-flight operator.

The handheld runtime

The runtime is client-driven. On the handheld:

  • The operator menu (the big-tile PWA menu) lists the active configurable processes as tiles, alongside the curated built-in screens.
  • Tapping a tile runs it at /process/:key. The client walks the screens locally, evaluates branch conditions, step skip conditions, and any compute steps (variable assignments) with a safe TypeScript interpreter (no round-trip per screen).
  • It posts to the server only at task steps (checkpoints). Screen and compute steps are fully offline. Checkpoints are offline-queued: a screen flow keeps working through a Wi-Fi blip, and the run holds at a task until connectivity returns.
  • Checkpoints are idempotent per (instance, step), so a device swap or a retry resumes cleanly without re-running a task's side effects.

Running a process needs PROCESS_DESIGN_VIEW (granted to all shipped roles).

Curated tasks, not arbitrary code

A process cannot run arbitrary Java. Task steps choose from a curated server-side task library, a fixed registry with no API to author new task types. Each task calls an existing WCS endpoint and forwards the operator's identity (X-Auth-*), so the same RBAC and warehouse scope apply as if the operator called it directly:

Task Calls
inventory.move POST /api/flow/moves
slotting.putaway POST /api/slotting/decant/putaway
picking.confirm POST /api/orders/pick-tasks/{id}/confirm
inventory.lookup GET /api/inventory/availability (by locationId) or GET /api/inventory/handling-units/{huId}/contents (by huId; sums the SKU's lines on the HU); falls back to warehouse-wide availability when neither is supplied. warehouseId is auto-injected from the running instance.
txlog.post POST /api/txlog/events
host.confirm (Phase 2) POST /api/host/inventory/adjustments
inventory.adjust (Phase 2) POST /api/txlog/events (a StockAdjusted event)
counting.capture (Phase 2) POST /api/counting/tasks/{taskId}/lines/{lineId}/station-count
order.lookup (Phase 2) GET /api/orders/{id}

A failing task surfaces the error and does not advance the instance.

warehouseId auto-injection. All curated tasks (except script) automatically receive the running instance's warehouseId without the designer mapping it. An explicit input mapping still wins. This means the operator never has to scan a warehouse, and task inputs like inventory.lookup only need the operator-visible identifiers (skuId, locationId or huId).

Compute steps (expression grammar)

The expression grammar used by a compute step's set[].expr is a superset of the condition grammar that adds arithmetic:

  • identifiers, number, string ('…'/"…"), and boolean (true/false) literals
  • arithmetic: +, -, *, / with unary minus and parentheses
  • comparisons: ==, !=, <>, <, <=, >, >=
  • boolean: and / or / not

No function calls, no member access. Examples: qty == expected or qty == prevCount, expected - qty. The server validates every expr with ExpressionParser at publish (422 on a parse error); additionally, every variable name an expression references must be declared in the data objectExpressionParser.identifiers extracts the referenced identifiers and the validator cross-checks them against the schema (422 if any is missing). The designer mirrors both validations inline: a syntax error is flagged immediately, and an undeclared target variable or expression variable is highlighted with a suggested fix ("Add it in Data object"). The task dialog's Done button is disabled until all rows pass both checks.

Scan verification

A screen's format checks (regex / maxLength / mustEqual) only check the shape of an entered value. Verification confirms the code actually exists in master data and pulls its linked ids, so a later task that needs a UUID gets it. Text and Number input screens can carry an optional Verify block:

"config": {
  "header": "Scan location",
  "writeTo": "locationCode",
  "verify": {
    "kind": "location",                 // "barcode" | "sku" | "location" | "skuScan"
    "write": {                          // per-kind resolved field -> data-object variable
      "id":   "locationId",             // the resolved UUID
      "code": "locationCode",
      "purpose": "locationPurpose"      // location also supports: purpose, locationType, status
    },                                   // SKU-like kinds use: id, code, name, uomCode, schemaCategory
    "onNotFound": { "mode": "reprompt" } // or { "mode": "goto", "step": "<stepId>" }
  }
}
  • kind picks what to resolve: barcode (a scanned barcode → SKU + UOMs + attribute-schema graph), sku (a SKU code), location (a location code), or skuScan (combined: tries the value as a product barcode first; if no match, tries it as a SKU code — a barcode pins its UOM, a single-UOM SKU auto-picks, a multi-UOM SKU triggers a UOM picker the operator uses before the step advances). The designer's kind picker is server-driven by capabilities.verifyKinds (["barcode","sku","location","skuScan"]).
  • write maps per-kind resolved field keys into declared data-object variables. The valid keys differ by kind (served via capabilities.verifyFields): locationid, code, purpose, locationType, status; barcode / sku / skuScanid, code, name, uomCode, schemaCategory. id is the resolved UUID. For skuScan, map uomCode to store the chosen or auto-resolved unit of measure. An invalid key for the chosen kind fails 422 at publish.
  • onNotFound is re-prompt (clear and ask again) or go to a step. A goto target must exist and counts toward reachability, so the validator never flags it as unreachable.

In the designer, a "Verify flow" button appears beside the live mockup whenever a scan-capable text or number input screen is selected and the server advertises at least one verifyKind. Clicking it opens a step-by-step guided dialog: ① pick the scan kind, ② map the per-kind resolved fields to data-object variables (the field rows are driven by capabilities.verifyFields so a location shows Purpose / Location type / Status, a SKU shows Description / Unit of measure / Attribute category), ③ choose the not-found behaviour. Switching kind in step ① drops any write mappings whose key is not valid for the new kind. When verification is already configured, the button reads "Edit verify flow" and a small "Verification on" label appears beside it; a "Turn off verification" action inside the dialog removes the verify block. The properties panel no longer carries the verify checkbox — it stays focused on the screen's own input fields.

On the curated-API path (no raw SQL). The runtime posts POST /api/process-designer/verify {warehouseId, kind, code} (PROCESS_DESIGN_VIEW), which proxies the read-only master-data resolve endpoints with the operator's forwarded identity (so master-data RBAC + warehouse scope apply) and returns a normalised {found, ambiguous, id, code, name, uomCode, schemaCategory, matchedAs, uoms, needsUomChoice, detail, fields}. The new fields map carries the per-kind resolved values keyed by the catalog keys (location: id/code/purpose/locationType/status; SKU-like: id/code/name/uomCode/schemaCategory); the runtime reads writes from fields first, falling back to the legacy top-level fields for older servers. The skuScan-specific fields: matchedAs signals whether a "barcode" or "sku" code path fired; uoms is the SKU's full unit list [{code, baseUnit}]; needsUomChoice is true when the SKU has more than one UOM and the runtime must prompt. The resolve endpoints return 200 with found:false on a miss (never 404), so a flow branches instead of erroring; a downstream failure surfaces as 502.

At runtime, on submit the client resolves: offline holds ("verification needs a connection"); found:false re-prompts or routes per onNotFound; found:true merges the write mappings into the data object and continues; ambiguous:true (a value spanning more than one SKU) shows a subtle note. For skuScan when needsUomChoice:true the normal screen is replaced by a UOM picker — a full-screen button list of the SKU's units of measure (each labeled with its code; the base unit is tagged) — and the step only advances once the operator selects one; the chosen code is written into the variable mapped to uomCode. Simulate mode resolves locally with no backend (a "simulate not found" toggle). Publish validates the block: a valid kind, every write key a known per-kind resolved field whose target is a declared variable (an invalid key for the chosen kind fails 422), and a goto step that exists.

The proxy's master-data base url is OPENWCS_MASTER_DATA_BASE_URL (openwcs.process-designer.master-data-base-url).

Sandboxed scripting (Phase 3, controlled escape hatch)

For logic the curated library does not cover, Phase 3 adds a built-in script task type. The step config is { "script": "<js>", "outputs": [{ "name": "..." }] }. The snippet runs server-side in a locked-down GraalJS sandbox:

  • No host access whatsoever: allowAllAccess(false), HostAccess.NONE, no host class lookup or loading, no native, no threads, no process, no IO (IOAccess.NONE), no environment, PolyglotAccess.NONE. It is interpreted guest JavaScript, never compiled to host JVM bytecode.
  • Hard limits: a statement limit (default 100000), a watchdog wall-clock timeout (default 2000 ms), and an output size cap (default 65536 bytes), all tunable via openwcs.process-designer.scripting.{statement-limit,timeout-ms,max-output-bytes}.
  • The script sees only a deep-frozen, read-only data global (the process data object) and returns an object whose fields become the declared outputs.

Governance is defense in depth. A script step can be saved or published only when both:

  1. the config flag openwcs.process-designer.scripting.enabled is on (env OPENWCS_PROCESS_SCRIPTING_ENABLED, default false), else 422; and
  2. the caller holds the new PROCESS_SCRIPT_AUTHOR permission (granted to ADMIN only), else 403.

Each script is parse-validated in the sandbox at publish (a malformed snippet is rejected with 422). The script step code editor in the designer is shown only when scripting is enabled and the caller may author scripts.

AI task-assist (Phase 3, design-time only, never auto-deploys)

The designer can ask an assistant to describe a task in plain language: POST /api/process-designer/assist/task with {description, variables:[{name,type}]} returns {kind:"curated"|"script"|"none", taskType?, inputs?, outputs?, script?, rationale, confidence} (gated by PROCESS_DESIGN_EDIT). It uses the Anthropic Java SDK (default model claude-haiku-4-5, key from the ANTHROPIC_API_KEY env), grounded with the live task catalog plus the available variables, and either:

  • maps the description to an existing curated task type with variable mappings (kind:"curated"), which you Apply; or
  • drafts a sandboxed JS snippet (kind:"script") for a human to review and Insert as a script step, clearly labelled "AI draft, for your review".

The suggestion is never saved, compiled, or executed by the server, and nothing is deployed automatically. AI text → compiled Java → a live server is permanently out of scope by design. Absent an Anthropic key, /assist/task returns 503 and the service still starts.

Capabilities

GET /api/process-designer/capabilities{scriptingEnabled, aiAssistEnabled, canAuthorScript, verifyKinds, verifyFields} (gated by PROCESS_DESIGN_VIEW) tells the designer whether to show the script editor and the AI assist panel, which scan-verify kinds the Verify picker offers (["barcode","sku","location","skuScan"]), and the per-kind resolvable field catalog (verifyFields) that drives the step-by-step Verify dialog's field rows in step 2.

New compose env on process-designer: OPENWCS_PROCESS_SCRIPTING_ENABLED (default false), ANTHROPIC_API_KEY (opt-in), OPENWCS_PROCESS_ASSIST_MODEL.

Instance monitoring (Phase 2)

A read-only desktop Process instances screen (Engineering, ADMIN / SUPERVISOR, route /process-instances) lists process instances newest-first with a detail pane (the data object, the current step, and the step trail). It is backed by GET /api/process-designer/instances?processKey=&status=&warehouseId=&limit= (summaries) plus the existing GET /api/process-designer/instances/{id} (detail).

Sample process

The service seeds an ACTIVE Stock Check process out of the box: scan location → scan SKU → count → txlog.post records a StockCounted event → acknowledge. It is a worked example of screens + a curated task in one flow.

Its data object contains the following fields:

Variable Type Purpose
location location Resolved location (typed)
sku sku Resolved SKU (typed)
qty number Counted quantity
skuBarcode string Raw barcode value scanned for the SKU
LocationCode string Location code as scanned
LocationBarcode string Raw barcode value scanned for the location
HUBarcode string Handling-unit barcode as scanned
HUCode string Handling-unit code as scanned

The five string variables (skuBarcode, LocationCode, LocationBarcode, HUBarcode, HUCode) capture the literal scanned codes before and alongside the resolution performed by the verify block; they are available to downstream tasks and {{placeholder}} text. Fresh installs receive them via the V1 seed; existing installations pick them up via the idempotent V3 migration.

Internationalisation

The designer and runtime are translated for German, French, Spanish, and Chinese (see Internationalisation).

See Mobile Process Designer for the full design-detail reference (model, screen-step config, API surface), Services for the service summary, Security for the identity-forwarding model, and Transport Overview / Slotting and Replenishment for the endpoints the curated tasks drive.

Clone this wiki locally