Automate expense compliance without hiding the human decision
Apply deterministic policy, archive the receipt, auto-approve ordinary claims, and route only exceptional expenses to Finance.
The workflow separates policy from discretion. Code calculates policy facts; markup makes the decision and durable human boundary obvious to reviewers.
The workflow at a glance
Text
Expense webhook
→ [Check policy ∥ Archive receipt]
→ Requires review? ─ yes → Finance approval
└ no → Auto-approve
→ Record decision
→ Notify accounting event subscribersThe workflow
WOML
<woml>
<workflow
id="expense-compliance"
name="Expense compliance"
description="Apply policy and hold exceptional expenses for Finance review."
version="1.0.0"
>
<config concurrency="12" timeout="72h" queue="finance" />
<triggers>
<webhook
id="submitExpense"
path="/webhooks/expenses"
method="POST"
auth="bearer"
secret="{{secrets.EXPENSE_WEBHOOK_TOKEN}}"
>
<schema>
{
"type": "object",
"required": ["expenseId", "employeeId", "category", "amount", "receiptUrl"],
"properties": {
"expenseId": { "type": "string" },
"employeeId": { "type": "string" },
"category": { "type": "string" },
"amount": { "type": "number", "minimum": 0 },
"receiptUrl": { "type": "string" }
}
}
</schema>
</webhook>
</triggers>
<steps>
<parallel id="preflight" name="Check expense" concurrency="2" on-error="wait-all">
<step id="policy" name="Evaluate policy">
<script>
const limits = { meals: 100, travel: 2000, equipment: 1500 };
const limit = limits[context.payload.category] ?? 250;
const requiresReview = context.payload.amount > limit;
return { limit, requiresReview, reason: requiresReview ? "above-category-limit" : "within-policy" };
</script>
</step>
<step id="receipt" name="Archive receipt" retry="3" retry-backoff="exponential">
<script>
const response = await services.http.request({
url: context.payload.receiptUrl,
responseType: "storage",
storage: {
key: `expense-receipts/${context.payload.expenseId}`,
overwrite: true
},
timeout: "30s"
}, { name: "archive-expense-receipt" });
return response.data;
</script>
</step>
</parallel>
<choose id="decision" name="Select compliance route">
<when test="{{context.steps.policy.requiresReview}}">
<approval
id="financeReview"
name="Review exceptional expense"
description="Finance must approve an expense above its category limit."
timeout="48h"
on-timeout="reject"
>
<notify>
<telegram chats="123456789" bot-token="{{secrets.TELEGRAM_BOT_TOKEN}}" />
<discord channels="200000000000000001" bot-token="{{secrets.DISCORD_BOT_TOKEN}}" />
</notify>
<when-approved>
<step id="approveExpense"><script>return { status: "approved", source: "finance" };</script></step>
</when-approved>
<when-rejected>
<step id="rejectExpense"><script>return { status: "rejected", source: "finance" };</script></step>
</when-rejected>
</approval>
<result value="{{context.steps.financeReview}}" />
</when>
<otherwise>
<step id="autoApproveExpense"><script>return { decision: "approved", source: "policy" };</script></step>
<result value="{{context.steps.autoApproveExpense}}" />
</otherwise>
</choose>
<step id="recordDecision" name="Record compliance decision" retry="3">
<script>
const db = services.db({ driver: "postgres", connection: secrets.POSTGRES_URL });
await db.insert({
table: "expense_decisions",
values: {
expense_id: context.payload.expenseId,
employee_id: context.payload.employeeId,
amount: context.payload.amount,
status: context.steps.decision.decision,
decision_source: context.steps.decision.source,
receipt_key: context.steps.receipt.key
}
}, { name: "record-expense-decision" });
return {
expenseId: context.payload.expenseId,
status: context.steps.decision.decision,
receipt: context.steps.receipt
};
</script>
</step>
<step id="publishDecision" name="Publish accounting event">
<script>
await services.events.emit(
"expense.decided",
context.steps.recordDecision,
{ name: "publish-expense-decision" }
);
return context.steps.recordDecision;
</script>
</step>
</steps>
</workflow>
</woml>Configure and run it
Terminal
woml secrets set EXPENSE_WEBHOOK_TOKEN
woml secrets set POSTGRES_URL
woml secrets set TELEGRAM_BOT_TOKEN
woml secrets set DISCORD_BOT_TOKEN
woml check expense-compliance.woml
woml run expense-compliance.womlReplace both notification destination IDs. A decision from either provider settles the same approval; WOML does not create two competing human tasks.
Why this shows WOML's range
The code can download and archive arbitrary receipts while the structure stays auditable. Policy routing, dual-provider approval, durable waiting, SQL history, and event publication are visible in one reviewable file rather than hidden across a canvas, credential nodes, and platform-specific state.