← All examples
Human operationsApprovalStorage

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 subscribers

The 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.woml

Replace 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.