Expense report approval
Overview
The Expense Report Approval workflow automates the full lifecycle of an employee expense claim: from submission and receipt upload, through manager approval and finance validation, to payment. It supports two clarification loops (manager → employee, and manager → finance) and two independent final-rejection paths, so that a rejection originated by the manager and a rejection originated during finance validation are tracked separately.
Download: expense-report-approval.okmflow
Supported execution paths:
- Direct approval: the request passes Manager Approval and Finance Validation without issues and the expense is paid.
- Employee clarification loop: the manager (either from Manager Approval or from Manager Review) returns the request to the employee for corrections. The request can loop back to Manager Approval as many times as needed.
- Finance ↔ Manager loop: Finance Validation rejects the request; it is routed to the same manager who originally approved it (Manager Review) instead of ending the process. From there the manager can request employee clarification, reply directly to Finance, or reject the request definitively.
- Rejection at Manager Approval: the manager rejects the request directly — the employee is notified by email and the process ends.
- Rejection at Manager Review (originated by Finance): the manager rejects the request after a Finance rejection — the employee is notified by email and the process ends via a separate terminal branch.
Diagram
Prerequisites
Before deploying this workflow, the following must exist in OpenKM.
Database
A custom counter table is used to generate sequential expense references. It is generic and shared with other workflows (e.g. Purchase Order) via different counter_key values — only create the table once:
CREATE TABLE IF NOT EXISTS APP_COUNTER (
counter_key VARCHAR(50) NOT NULL,
counter_value INT NOT NULL DEFAULT 1,
CONSTRAINT pk_app_counter PRIMARY KEY (counter_key)
);
INSERT INTO APP_COUNTER (counter_key, counter_value)
VALUES ('expense_report', 1);
Users and roles
| Identifier | Type | Used in | Description |
|---|---|---|---|
|
|
Role |
Finance Validation |
Pool of finance validators. Any member can self-assign the task the first time it runs. Must have at least one user assigned. |
|
|
User |
Manager Approval |
Assigned when the requesting department is IT. |
|
|
User |
Manager Approval |
Assigned when the requesting department is Finance. |
|
|
User |
Manager Approval |
Assigned when the requesting department is Operations. |
|
|
User |
Manager Approval |
Assigned when the requesting department is Sales. |
|
|
User |
Manager Approval |
Assigned when the requesting department is HR. |
|
|
User |
Payment Processing |
Single user who processes all payments. Fixed direct assignment (not role/department based). |
|
(workflow initiator) |
— |
Expense Submission, Employee Edit & Resubmit |
The employee who started the workflow; resolved dynamically via |
Manager Approval and Manager Review are always resolved to the same physical person: Manager Review re-assigns via task history lookup (WorkflowUtils.getTaskInstances) rather than re-computing the department mapping, so it always lands on whoever actually approved the request, even if the department→manager mapping changes later.
Metadata group
The property group okg:expense_report must be registered in OpenKM before the first execution. It is applied automatically to the expense record by the workflow — the user never fills it in manually.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE property-groups PUBLIC "-//OpenKM//DTD Property Groups 3.12//EN"
"http://www.openkm.com/dtd/property-groups-3.12.dtd">
<property-groups>
<property-group label="Expense Report" name="okg:expense_report" readonly="true">
<input label="Expense Reference" name="okp:expense_report.expenseNumber" />
<input label="Employee" name="okp:expense_report.employee" />
<select label="Department" name="okp:expense_report.department" type="simple">
<option label="IT" value="it"/>
<option label="Finance" value="finance"/>
<option label="Operations" value="operations"/>
<option label="Sales" value="sales"/>
<option label="HR" value="hr"/>
</select>
<input label="Expense Date" name="okp:expense_report.expenseDate" type="date" />
<select label="Category" name="okp:expense_report.category" type="simple">
<option label="Travel" value="travel"/>
<option label="Meals" value="meals"/>
<option label="Accommodation" value="accommodation"/>
<option label="Office Supplies" value="office_supplies"/>
<option label="Other" value="other"/>
</select>
<input label="Amount" name="okp:expense_report.amount" />
<select label="Currency" name="okp:expense_report.currency" type="simple">
<option label="EUR" value="EUR"/>
<option label="USD" value="USD"/>
<option label="GBP" value="GBP"/>
</select>
<textarea label="Description" name="okp:expense_report.description" dbColumnSize="1024" />
<textarea label="Manager Comments" name="okp:expense_report.managerComments" dbColumnSize="1024" />
<textarea label="Finance Comments" name="okp:expense_report.financeComments" dbColumnSize="1024" />
<input label="Payment Date" name="okp:expense_report.paymentDate" type="date" />
<input label="Payment Reference" name="okp:expense_report.paymentReference" />
<select label="Status" name="okp:expense_report.status" type="simple">
<option label="Pending Manager Approval" value="pending_manager_approval"/>
<option label="Clarification Requested" value="clarification_requested"/>
<option label="Pending Finance Validation" value="pending_finance_validation"/>
<option label="Rejected - Manager" value="rejected_manager"/>
<option label="Rejected - Finance" value="rejected_finance"/>
<option label="Rejected - Finance and Manager" value="rejected_by_finance_and_manager"/>
<option label="Approved" value="approved"/>
<option label="Paid" value="paid"/>
</select>
</property-group>
</property-groups>
The group is readonly="true" — all writes are performed programmatically from Action nodes in the workflow.
Repository structure
The action Create Expense Record (the first node after Start) builds the following structure in the OpenKM repository each time a new workflow instance is launched:
/okm:root/
--- Expense Report Approval/
--- {year}/ - Folder (type: folder)
--- RECEIPTS-{year}-{NNNNN}/ - Record (type: record) - the main expediente
Reference format: RECEIPTS-{year}-{NNNNN} where NNNNN is a zero-padded 5-digit sequential number (e.g. RECEIPTS-2026-00001). The counter is read from APP_COUNTER.counter_key = 'expense_report', incremented, and saved back atomically at workflow start.
The workflow process instance is linked to this record via WorkflowUtils.setProcessInstanceNode(context, recordUuid), so the workflow is accessible directly from the document explorer.
Unlike the Purchase Order workflow, there is no subfolder for uploads: every document attached to the record is a receipt, so both the initial and additional receipt uploads go straight to the root of the record.
Node reference
Start
Launches the workflow. No form is presented to the user at this point — the process starts immediately and transitions to the Create Expense Record action.
Create Expense Record - Action
Runs immediately after Start. Performs all repository initialisation:
- Reads and increments the counter from
APP_COUNTER(counter_key = 'expense_report'), buildsRECEIPTS-{year}-{NNNNN}. - Creates the year folder
/okm:root/Expense Report Approval/{year}if it does not already exist. - Creates the expense record inside the year folder using
ws.record.create(fldUuid, expenseNumber, "", 0). - Stores
expenseNumber,expenseRecordPath,expenseRecordUuidand anUploaddescriptor (receiptsUploadData, pointing at the record root) in the workflow context. - Links the process instance to the record via
WorkflowUtils.setProcessInstanceNode(context, recordUuid).
No metadata is written yet — the record does not have a metadata group at this point.
Expense Submission - Task
Assignment: the workflow initiator (initiator.getId()).
| Field | Type | Required | Notes |
|---|---|---|---|
|
Department |
Select |
Yes |
|
|
Expense Date |
Date |
Yes |
— |
|
Category |
Select |
Yes |
|
|
Amount |
Text (numeric, > 0) |
Yes |
— |
|
Currency |
Select |
Yes |
|
|
Description |
Textarea |
Yes |
— |
|
Receipts |
Upload ( |
No |
Uploaded directly to the record root. |
| Button | Transition | Description |
|---|---|---|
|
Submit |
|
Proceeds to Save Expense Metadata. |
Save Expense Metadata - Action
Runs immediately after Expense Submission. Reads all form values from context, resolves the employee's display name from initiator.getName(), and writes the first version of okg:expense_report (using addGroup, since the group does not exist yet). Sets status = pending_manager_approval.
Important: expenseDate is written to metadata as-is (expenseDateInput.getValue()), without any reformatting. Date-type form fields already store the value in the yyyyMMddHHmmss format that the metadata engine expects; reformatting it (e.g. to yyyy-MM-dd) before calling setProperties/addGroup triggers a validation error (see Key patterns).
Manager Approval - Task
Assignment logic (department-based):
| Department | Assigned user |
|---|---|
|
IT |
|
|
Finance |
|
|
Operations |
|
|
Sales |
|
|
HR |
|
The form shows the full expense in read-only mode, a clickable link to the expense record (type="folder"), and a single free-text Manager Comments field used across every outcome (approval note, clarification reason, or rejection reason).
| Button | Transition | Description |
|---|---|---|
|
Approve |
|
Proceeds to Set status - Pending Finance → Finance Validation. |
|
Request Clarification |
|
Proceeds to Set status - Clarification Requested → Mail - Clarification Requested → Employee Edit & Resubmit. |
|
Reject |
|
Proceeds to Set status - Rejected by manager → Mail - Final Rejection by manager → End by manager. Terminal. |
Set status - Pending Finance - Action
Updates okp:expense_report.status to pending_finance_validation and stores the manager's approval note (managerComments), then routes to Finance Validation.
Finance Validation - Task
Assignment logic (pool → direct re-assignment via task history):
1. Query task history: WorkflowUtils.getTaskInstances(piId, "Finance Validation")
2. If a previous instance with an assigned actor exists → return that actor (String, direct assignment)
(ensures the same finance user handles re-entry after the "Reply to Finance" loop)
3. If first time → return the pool of all ROLE_FINANCE users (List<String>), any of whom must self-assign
The form shows the expense in read-only mode, the manager's reply if this is a re-entry (pre-loaded from managerComments), and an editable Finance Comments field.
| Button | Transition | Description |
|---|---|---|
|
Approve |
|
Proceeds to Set status - Approved → Payment Processing. |
|
Reject |
|
Proceeds to Manager Review (not a terminal state — routed back to the approving manager). |
Set status - Approved - Action
Updates okp:expense_report.status to approved and stores financeComments, then routes to Payment Processing.
Manager Review - Task
Reached only when Finance Validation is rejected. This is not a new department-based assignment: it is always routed to the same person who completed Manager Approval, via task-history lookup (see Key patterns), because that manager already vetted the request once and is best placed to judge whether Finance's objection needs the employee's input or can be resolved directly.
The form shows the expense in read-only mode, Finance's rejection reason (read-only), and an editable Manager Comments field.
| Button | Transition | Description |
|---|---|---|
|
Request Employee Clarification |
|
Proceeds to Set status - Clarification Requested → Mail - Clarification Requested → Employee Edit & Resubmit (same shared path used by Manager Approval). |
|
Reject |
|
Proceeds to Set status - Rejected by finance and manager → Mail - Final Rejection by finance and manager → End by finance and manager. Terminal, separate from a direct Manager Approval rejection. |
|
Reply to Finance |
|
Proceeds to Set status - Pending Finance (Manager Reply) → back to Finance Validation (re-assigned to the same finance user). |
Set status - Clarification Requested - Action
Shared by both Manager Approval and Manager Review. Updates okp:expense_report.status to clarification_requested and stores managerComments, then routes to Mail - Clarification Requested. Centralising this in one node means the metadata-write logic for "clarification requested" only exists once, regardless of which task triggered it.
Mail - Clarification Requested - Mail
| Field | Value |
|---|---|
|
Recipients |
|
|
Subject |
|
|
Body |
Includes the expense reference and the manager's comments ( |
Employee Edit & Resubmit - Task
Assignment: the workflow initiator (initiator.getId()).
Shows the manager's clarification request (read-only) and every field from Expense Submission, pre-filled and editable (same field names as the original submission, so the values the employee resubmits overwrite the originals in context). Also allows uploading additional receipts to the same record (previously uploaded files are preserved).
| Button | Transition | Description |
|---|---|---|
|
Resubmit |
|
Proceeds to Set status - Pending Manager → Manager Approval. |
Set status - Pending Manager - Action
Re-reads the (possibly edited) form fields from context and re-writes the full metadata set (department, expense date, category, amount, currency, description), plus status = pending_manager_approval. This is what makes the resubmission loop actually persist the employee's edits to the record, not just to the in-memory workflow context.
Set status - Pending Finance (Manager Reply) - Action
Updates okp:expense_report.status to pending_finance_validation and stores the manager's reply (managerComments), then routes back to Finance Validation (which re-assigns to the same finance user via task history).
Payment Processing - Task
Assignment: direct, fixed to a single user, paymentManager.
Shows the full expense in read-only mode, with "Pay to (Employee)" as the first field so the payer immediately knows the recipient, plus editable Payment Date and Payment Reference fields.
| Button | Transition | Description |
|---|---|---|
|
Mark as Paid |
|
Proceeds to Set status - Paid → Mail - Payment Completed → End. |
Set status - Paid - Action
Updates okp:expense_report.status to paid, and stores paymentDate (raw yyyyMMddHHmmss) and paymentReference. Separately formats paymentDate to yyyy-MM-dd and stores it in context as paymentDate_fmt, exclusively for display in the notification email — the stored metadata property always keeps the raw format.
Mail - Payment Completed - Mail
| Field | Value |
|---|---|
|
Recipients |
|
|
Subject |
|
|
Body |
Payment date ( |
Set status - Rejected by manager / Mail - Final Rejection by manager / End by manager
Direct-rejection terminal branch from Manager Approval. Updates okp:expense_report.status to rejected_manager and stores managerComments, emails the employee, and ends the process.
Set status - Rejected by finance and manager / Mail - Final Rejection by finance and manager / End by finance and manager
Terminal branch from Manager Review (i.e. rejection that started at Finance Validation). Kept as an independent branch from the one above so the two rejection origins can be distinguished and reported on separately.
Known gap at time of writing: this branch currently writes the same status value (rejected_manager) and the same email wording ("...rejected by your manager") as the direct-rejection branch above, instead of the more specific rejected_by_finance_and_manager value already defined in the metadata group. Until this is corrected in the workflow, reporting cannot yet distinguish this branch from a direct Manager Approval rejection by status value alone — only by which End node was reached.
Metadata lifecycle
The okp:expense_report.status field is updated automatically at each stage transition. The complete lifecycle is:
pending_manager_approval → clarification_requested → pending_manager_approval (loop, repeatable)
pending_manager_approval → rejected_manager (terminal)
pending_manager_approval → pending_finance_validation
pending_finance_validation → pending_finance_validation (via Manager Review → Reply to Finance loop)
pending_finance_validation → clarification_requested → pending_manager_approval (via Manager Review)
pending_finance_validation → rejected_by_finance_and_manager (terminal, via Manager Review)
pending_finance_validation → approved
approved → paid (terminal)
Key patterns
| Pattern | Where applied |
|---|---|
|
Counter table for reference numbering |
|
|
No upload subfolder when everything is one document type |
Unlike Purchase Order ( |
|
Raw date format for metadata properties |
Date fields must be written to |
|
Pool → direct re-assignment via task history |
|
|
Cross-task re-assignment via task history |
|
|
Single shared "set status" action for multiple entry points |
Set status - Clarification Requested is targeted by a |
|
Department-based manager assignment |
|
|
Fixed single-user assignment |
Payment Processing uses |
|
Independent terminal branches for the same conceptual outcome |
Rejection from Manager Approval and rejection from Manager Review use separate Action/Mail/End node triples, so the two origins remain distinguishable by which End node was reached, even though (at time of writing) the status value written is not yet distinct — see the note under Manager Review's rejection branch above. |
|
|
Read-only clickable link to the expense record, used in every review-style task form. |
|
All-fields-editable resubmission |
Employee Edit & Resubmit reuses the exact field names from Expense Submission with |
|
Metadata written by Action nodes, not users |
|
|
Workflow linked to repository record |
|