Category-based expense thresholds let an OIC AI Agent apply a different approval limit per expense type — Travel, Meals, Training, Software — instead of one flat rule for every claim, so a $800 flight and a $800 team dinner are judged against the policy that actually fits them, not the same arbitrary number.
Have you ever wondered why a single expense limit for everything is a bad idea?
In our earlier tutorial, we built an agent with one flat rule: expenses above $500 are rejected.
That works as a starting point — but real company finance policies are never that simple.
Should a $600 flight ticket really be rejected the same way as a $600 team dinner?
Of course not. A flight is expected to cost more. A dinner for one person at $600 is a very different story.
The answer is category-based thresholds — and that is exactly what we build today.
🗺️ The Big Picture — What Changes in This Session?
We are upgrading from a single flat threshold to a fully category-aware approval engine.
Here is how data flows through the upgraded system:
Expense
Submitted
Category
Detected
Switch
Branches
Amount
Checked
Decision
Mapped
Agent
Responds
Simple, right? Now let us understand each part in detail! 🎯
📌 Part 1 — Why Category Thresholds?
A flat $500 limit treats all expenses equally — and that is exactly the problem.
Different categories have completely different normal ranges.
Imagine a supermarket where every item costs exactly the same — say, $10.
A chocolate bar for $10? Fine. A whole chicken for $10? Reasonable. A TV for $10? Something is very wrong.
Real pricing works differently because different things have different normal ranges. Expense categories work exactly the same way.
A $800 flight is completely normal. A $800 office pen is deeply suspicious. Category-based thresholds teach your agent to know the difference. ✅
🔍 Our New Policy Table
Here is the policy we will encode into the integration.
Each category has its own limit, and "Other" is a conservative catch-all for anything not listed.
| Expense Category | Approved Limit (USD) | Real-World Reasoning |
|---|---|---|
| Travel | $1,000 | Flights, hotels, taxis — legitimately expensive |
| Meals | $75 | A single meal for one or two people |
| Office Supplies | $200 | Stationery, peripherals, small equipment |
| Training | $500 | Online courses, certifications, books |
| Software | $300 | SaaS subscriptions, licence fees |
| Entertainment | $150 | Client events, team activities |
| Unknown / Other | $100 | Conservative default for unclassified expenses |
- The REST Integration gets a smarter Switch that checks both amount and category.
- The request payload now requires
expenseCategory— it was optional before. - The response payload now includes
categoryLimitso the agent can report the exact rule applied. - The system prompt is updated to always ask for category if it is missing.
- The tool description is updated to reflect the new required input.
The agent itself — the LLM, the ReAct pattern, the endpoint — stays exactly the same. This is the beauty of the architecture: the AI layer does not change when business rules change. 🚀
🛠️ Part 2 — Upgrading the Integration — Step by Step
We do NOT rebuild from scratch. We version the existing integration and modify it.
This is the professional way to extend OIC integrations in production. Here is the roadmap:
expenseCategory to the required array and enforce it with an enum list of valid values.concat() to build dynamic, human-friendly reason messages that include actual numbers.In OIC , before changing an active integration, create a new version first. Go to the three-dot menu (⋮) on
CheckExpenseApproval → Create New Version.
This gives you v2.0 to edit while v1.0 stays live and handles real traffic.
If anything goes wrong with v2.0, you switch the agent back to v1.0 in seconds — no panic, no emergency.
Let us build each update now! 🚀
⌨️ Update 1 — New Request Payload (Category is Now Required)
In Part 1, expenseCategory was optional.
Now it is required because the decision depends entirely on it.
We update the request JSON Schema to mark it as mandatory and add an enum for validation.
This is the new shape of data the integration expects. Notice
expenseCategory is now listed in the required array alongside expenseAmount.
If a caller sends the request without a category, OIC returns a validation error
before the integration even starts — protecting your Switch logic from receiving an empty value.
The enum list acts like a dropdown: the caller can only pick from these exact options.
This prevents typos like "Traveel" or "meals" (wrong case) from silently hitting the wrong branch.
{
"type": "object",
"properties": {
"expenseAmount": {
"type": "number",
"description": "The expense amount in USD"
},
"expenseCategory": {
"type": "string",
"description": "Category of the expense",
"enum": ["Travel", "Meals", "Office Supplies",
"Training", "Software", "Entertainment", "Other"]
},
"submittedBy": {
"type": "string",
"description": "Employee name or ID (optional, for audit trail)"
}
},
"required": ["expenseAmount", "expenseCategory"]
}
The
enum field is a list of allowed values — think of it like a dropdown menu.
The caller can only pick from these exact options.
OIC validates the input automatically before passing it to your integration.
This means your Switch logic is always safe — it will never receive an unexpected category value.
⌨️ Update 2 — New Response Payload (Includes Category Limit)
We add two new fields to the response: categoryLimit and expenseCategory.
This matters greatly for the AI agent — it can now say exactly which rule was applied.
With
categoryLimit in the response, the agent can say:
"Your $800 Travel expense was APPROVED because the Travel category allows up to $1,000."
Without this field, the agent would have to guess the limit — which is unreliable.
Always give the agent everything it needs to explain the decision clearly and confidently.
Think of it as writing a detailed report card, not just a pass/fail grade. 📊
{
"type": "object",
"properties": {
"decision": {
"type": "string",
"description": "APPROVED or REJECTED"
},
"reason": {
"type": "string",
"description": "Full explanation of the decision"
},
"amount": {
"type": "number",
"description": "The submitted expense amount"
},
"expenseCategory": {
"type": "string",
"description": "The category that was evaluated"
},
"categoryLimit": {
"type": "number",
"description": "The policy limit that applies to this category"
},
"remainingBudget": {
"type": "number",
"description": "How much budget remains after this expense (if approved)"
}
}
}
⌨️ Update 3 — The New Switch Logic (Multi-Category Branching)
This is the heart of the upgrade.
In the OIC integration canvas, we replace the single If/Else with a Switch activity
that has one branch per category.
Imagine a sorting machine at a post office. Every parcel comes in on a conveyor belt. The machine reads the destination label (the category) and sends the parcel down the matching chute — London, Paris, Tokyo, or Other.
At the bottom of each chute, a different agent checks the parcel's weight against that destination's rules and stamps it OK or Return to Sender.
That is exactly how our new Switch works. The category is the destination label. Each branch is a chute. The amount check at the bottom of each branch is the weight inspection. 📦
🔍 Step-by-Step: Configuring the Switch in OIC Canvas
- Open
CheckExpenseApproval v2.0in the integration canvas. - Delete the old If/Else activity.
- Click + after the trigger → choose Switch.
- The Switch appears with one default branch. We will add more.
- Click + Add Branch for each category. You need six named branches plus the Otherwise branch.
🔍 Branch Conditions — Copy These Into Each Branch
For each branch, set two conditions joined with AND: the category must match, and the amount must be within limit.
Here are all the conditions as XPath expressions for the OIC mapper.
Each expression is the condition you type into a Switch branch in OIC. The first part checks the category name using
lower-case() — making it case-insensitive,
so "travel", "Travel", and "TRAVEL" all match the same branch.
The second part checks if the amount is within the limit for that category.
When BOTH conditions are true, the branch runs and returns APPROVED.
When the amount exceeds the limit, a nested If inside the branch returns REJECTED.
We show the full nested pattern for Travel below — all other categories follow the same structure.
lower-case($Request/expenseCategory) = 'travel'
IF: $Request/expenseAmount <= 1000
→ decision="APPROVED", categoryLimit=1000,
reason="Travel expense within $1,000 limit"
ELSE:
→ decision="REJECTED", categoryLimit=1000,
reason="Travel expense exceeds $1,000 limit"
lower-case($Request/expenseCategory) = 'meals'
IF: $Request/expenseAmount <= 75
→ decision="APPROVED", categoryLimit=75,
reason="Meals expense within $75 limit"
ELSE:
→ decision="REJECTED", categoryLimit=75,
reason="Meals expense exceeds $75 limit"
lower-case($Request/expenseCategory) = 'office supplies'
IF: $Request/expenseAmount <= 200
→ decision="APPROVED", categoryLimit=200,
reason="Office Supplies expense within $200 limit"
ELSE:
→ decision="REJECTED", categoryLimit=200,
reason="Office Supplies expense exceeds $200 limit"
lower-case($Request/expenseCategory) = 'training'
IF: $Request/expenseAmount <= 500
→ decision="APPROVED", categoryLimit=500,
reason="Training expense within $500 limit"
ELSE:
→ decision="REJECTED", categoryLimit=500,
reason="Training expense exceeds $500 limit"
lower-case($Request/expenseCategory) = 'software'
IF: $Request/expenseAmount <= 300
→ decision="APPROVED", categoryLimit=300,
reason="Software expense within $300 limit"
ELSE:
→ decision="REJECTED", categoryLimit=300,
reason="Software expense exceeds $300 limit"
lower-case($Request/expenseCategory) = 'entertainment'
IF: $Request/expenseAmount <= 150
→ decision="APPROVED", categoryLimit=150,
reason="Entertainment expense within $150 limit"
ELSE:
→ decision="REJECTED", categoryLimit=150,
reason="Entertainment expense exceeds $150 limit"
IF: $Request/expenseAmount <= 100
→ decision="APPROVED", categoryLimit=100,
reason="Expense within default $100 limit for uncategorised expenses"
ELSE:
→ decision="REJECTED", categoryLimit=100,
reason="Expense exceeds $100 default limit. Please specify a category."
Always wrap the category field in
lower-case() when comparing in your Switch conditions.
This means your integration never breaks if the LLM sends "Meals" vs "meals" vs "MEALS".
Defensive coding in the integration keeps the entire agent resilient against LLM variation. ✅
⌨️ Update 4 — The Response Mapping (Full Example for Travel Branch)
Here is what a complete response mapping looks like for the Travel APPROVED branch.
You configure this in the OIC Mapper activity that follows the nested If inside the Travel branch.
All other categories follow the exact same pattern — just swap the values.
The Mapper in OIC lets you drag-and-drop fields from the input to the output. Some values come from the request (like
amount and expenseCategory),
some are hardcoded strings (like decision and reason),
and remainingBudget is calculated on the fly using a simple subtraction.
The concat() function joins multiple text pieces together — like gluing words together with invisible tape.
The result is a human-friendly message that includes the actual numbers.
decision → "APPROVED"
reason → concat(
"Travel expense of $",
string($Request/expenseAmount),
" is approved. Travel category allows up to $1,000."
)
amount → $Request/expenseAmount
expenseCategory → $Request/expenseCategory
categoryLimit → 1000
remainingBudget → 1000 - $Request/expenseAmount
For the REJECTED branch, we change the decision and reason, and set
remainingBudget to 0 because the expense was not approved.
Notice how the reason message uses concat() to include the actual submitted amount
and the category limit — giving the employee clear, specific feedback
instead of a generic "expense rejected" message.
decision → "REJECTED"
reason → concat(
"Travel expense of $",
string($Request/expenseAmount),
" exceeds the Travel category limit of $1,000.",
" Please obtain manager pre-approval before resubmitting."
)
amount → $Request/expenseAmount
expenseCategory → $Request/expenseCategory
categoryLimit → 1000
remainingBudget → 0
⌨️ Update 5 — Reactivate v2.0 and Register it with the Agent
Once the Switch logic and all mappers are configured, we activate the new version and point the agent's tool at it.
- In the project, find
CheckExpenseApproval v2.0. - Click ⋮ → Activate. Enable Tracing. Confirm.
- Open the ExpenseApprovalAgent.
- Find the
check_expense_approvaltool → click Edit. - Under Integration, change the selected version from 1.0 to 2.0.
- Update the tool description (see next section).
- Save the agent.
Updating the integration version inside a tool is a one-click operation in OIC . The agent's LLM configuration, thinking pattern, system prompt, and endpoint URL all stay exactly the same. Only the tool's backing integration version changes. This is exactly why the architecture separates the agent from the tool — upgrades are painless. ✅
🏷️ Part 3 — Updated Tool Description
The tool description is what the LLM reads to decide when and how to call our tool.
Now that expenseCategory is required, we must update it so the agent
knows to collect the category before calling the tool.
This description now tells the agent three new things compared to Part 1: (1)
expenseCategory is required, not optional,
(2) what the valid category values are (so it never guesses),
and (3) what the per-category limits are (so it can answer policy questions without calling the tool).
The agent uses this knowledge to ask the user for the category if they forgot to mention it —
before calling the tool. This prevents unnecessary tool calls that would fail due to missing input.
Use this tool to evaluate whether an employee expense should be
approved or rejected based on both the amount AND the expense category.
REQUIRED INPUTS:
- expenseAmount (number): The expense amount in US dollars
- expenseCategory (string): Must be one of:
Travel | Meals | Office Supplies | Training |
Software | Entertainment | Other
CATEGORY LIMITS (for your reference when speaking to the user):
- Travel: up to $1,000
- Meals: up to $75
- Office Supplies: up to $200
- Training: up to $500
- Software: up to $300
- Entertainment: up to $150
- Other: up to $100
TOOL RETURNS:
- decision: APPROVED or REJECTED
- reason: Full explanation including the category limit applied
- amount: The submitted expense amount
- expenseCategory: The category that was evaluated
- categoryLimit: The policy limit for that category
- remainingBudget: How much budget remains (0 if rejected)
IMPORTANT:
- If the user has not provided expenseCategory, ask for it before calling this tool.
- Do not guess or assume the category — always confirm it with the user.
- Never call this tool without both required inputs.
📝 Part 4 — Updated System Prompt
We update the system prompt to reflect three new behaviours:
always ask for category if missing, use the category limits in the response,
and explain the policy clearly without needing a tool call.
The key additions are: the CATEGORY POLICY section (so the agent knows the limits even before calling the tool — useful for answering "what is the meal limit?" type questions), the rule to always ask for category before calling the tool, and the updated RESPONSE FORMAT that now includes the category name and its limit. The agent now gives answers that feel much more tailored — mentioning the exact category rule rather than a generic limit.
You are an Expense Approval Assistant for the company's finance department.
You use the check_expense_approval tool to evaluate all expense claims.
CATEGORY POLICY (for reference — use this for policy questions too):
- Travel: up to $1,000 per claim
- Meals: up to $75 per claim
- Office Supplies: up to $200 per claim
- Training: up to $500 per claim
- Software: up to $300 per claim
- Entertainment: up to $150 per claim
- Other: up to $100 per claim
BEFORE CALLING THE TOOL:
1. Make sure you have both the expense amount AND the expense category.
2. If the user has not mentioned the category, ask politely:
"Could you please tell me the expense category?
(Travel, Meals, Office Supplies, Training, Software,
Entertainment, or Other)"
3. Only call the tool once you have both pieces of information.
HOW TO RESPOND AFTER CALLING THE TOOL:
1. State the decision clearly: APPROVED or REJECTED.
2. Mention the expense amount and category.
3. State the category limit that was applied.
4. If approved, mention the remaining budget from the tool response.
5. If rejected, suggest the correct next step (e.g. seek manager approval).
6. Be professional, warm, and concise.
RESPONSE FORMAT EXAMPLE:
"APPROVED — Your [Category] expense of $[Amount] has been approved.
The [Category] category allows up to $[Limit].
You have $[RemainingBudget] remaining in this category for this claim."
RULES:
- Never call the tool without both expenseAmount and expenseCategory.
- Never make up category limits — always use what the tool returns.
- If the user asks about policy limits without submitting an expense,
answer from the CATEGORY POLICY section above — no tool call needed.
- Always be respectful, clear, and helpful.
🧪 Part 5 — Testing the Upgraded Agent
Now let us run through every important scenario.
A good test suite covers approved cases, rejected cases, missing category,
and boundary amounts (exactly at the limit). Let us run all five! 🎯
Test 1 — Travel, Within Limit ✅
We send a $800 Travel expense. The limit is $1,000. The agent should call the tool, get APPROVED, and return a message that mentions the Travel limit of $1,000 and the $200 remaining budget.
POST .../agents/EXPENSEAPPROVALAGENT/2.0/sessions
{
"sessionId": "test-travel-ok",
"userMessage": "Please review my travel expense of $800 for flights to Singapore."
}
Expected: "APPROVED — Your Travel expense of $800.00 has been approved. The Travel category allows up to $1,000 per claim. You have $200.00 remaining in this category for this claim. Please attach your flight receipt when submitting through the expense portal."
Test 2 — Meals, Exceeds Limit ❌
We send a $120 Meals expense. The limit is $75. The agent should detect the category, call the tool, get REJECTED, and explain the $75 Meals policy limit clearly. This is the critical test — $120 would have been APPROVED under the old flat $500 rule, but is now correctly REJECTED for the Meals category. This proves the category logic works.
POST .../agents/EXPENSEAPPROVALAGENT/2.0/sessions
{
"sessionId": "test-meals-reject",
"userMessage": "I had a team lunch for $120. Can this be approved?"
}
Expected: "REJECTED — Your Meals expense of $120.00 exceeds the Meals category limit of $75.00. To proceed with this expense, please obtain written approval from your department manager and resubmit with the approval reference number attached."
Test 3 — Missing Category (Agent Asks First) 💬
We send an expense amount but deliberately leave out the category. The agent should NOT call the tool yet. Instead, it should ask the user for the category first. This tests that the "BEFORE CALLING THE TOOL" rule in our system prompt is working correctly. An agent that calls the tool without a category would silently hit the Otherwise branch every time — a hard-to-detect bug that would approve everything up to $100 regardless of what was submitted.
POST .../agents/EXPENSEAPPROVALAGENT/2.0/sessions
{
"sessionId": "test-missing-category",
"userMessage": "I need to get $450 approved."
}
Expected: "Of course! Before I can check your expense, I need one more detail. Could you please tell me the expense category? The available options are: Travel, Meals, Office Supplies, Training, Software, Entertainment, or Other."
If the agent asks for the category instead of guessing or calling the tool blindly, your system prompt is working perfectly. This is exactly the behaviour that makes the agent safe and reliable in production. An agent that waits for complete information is a trustworthy agent. ✅
Test 4 — Boundary Amount (Exactly at Limit) 🎯
We send exactly $200 for Office Supplies — right at the boundary. This tests whether your Switch condition uses
<= (less than or equal) correctly.
An amount exactly equal to the limit should be APPROVED.
Off-by-one errors in boundary conditions are one of the most common bugs in integration logic.
Always test the exact boundary value — never assume it works without checking!
POST .../agents/EXPENSEAPPROVALAGENT/2.0/sessions
{
"sessionId": "test-boundary",
"userMessage": "I need approval for an office supplies purchase of exactly $200."
}
Expected: "APPROVED — Your Office Supplies expense of $200.00 has been approved. The Office Supplies category allows up to $200.00 per claim. This expense is exactly at the policy limit, so it qualifies for approval."
Test 5 — Policy Question (No Tool Call Needed) 📚
We ask a general policy question — not submitting an expense, just asking what the limits are. The agent should answer from its system prompt knowledge and NOT call the tool at all. This tests that the agent can distinguish between "tell me the policy" (answer from memory) and "approve this expense" (call the tool). An agent that calls the tool unnecessarily wastes resources and slows response time.
POST .../agents/EXPENSEAPPROVALAGENT/2.0/sessions
{
"sessionId": "test-policy-question",
"userMessage": "What is the limit for software purchases?"
}
Expected: "The Software category has a limit of $300 per claim. This covers SaaS subscriptions, licence fees, and similar software purchases. If you have a software expense to submit, please share the amount and I will process it for you right away."
✅ Part 6 — Best Practices for OIC AI Agents
Before you ship this to production, here are the rules experienced OIC developers live by.
DOs first, then DON'Ts — follow these and you will save yourself hours of debugging pain! 😅
- ✅ Start tiny. One tool. One decision. One test. Then expand. Complexity should grow with confidence, not before it.
- ✅ Write detailed tool descriptions. The LLM reads these to decide when to call your tool. Vague descriptions lead to missed calls or wrong calls.
- ✅ Set Temperature to 0.0 for business agents. You want consistent, deterministic decisions — not creative ones.
- ✅ Enable Tracing on all integrations. You will thank yourself when debugging at 11pm.
- ✅ Test the rejection path as carefully as the approval path. Your users will send edge cases you did not imagine.
- ✅ Version your agents. When you make changes, create a new version (v1.1) rather than overwriting. Clean rollback when things break.
- ❌ Don't put business logic in the system prompt alone. Always back it up with a tool that enforces the rule in code. The LLM can "forget" instructions under certain phrasings.
- ❌ Don't grant the agent more tools than it needs. More tools = more decisions = more chances to pick the wrong one. Keep it focused.
- ❌ Don't use Temperature > 0.2 for financial or legal decisions. Higher temperatures introduce randomness that is unacceptable in regulated contexts.
- ❌ Don't skip the monitoring step. Looking at traces is how you discover the agent is calling the tool with wrong parameters.
- ❌ Don't hardcode credentials in integrations. Always use OIC Connections with Resource Principals or OCI Vault secrets.
When testing with real category names and amounts — never use real employee data in development environments. Use synthetic test data (e.g. "John Smith, $800 travel") for all QA runs. OCI provides Data Safe and OCI Vault to help protect sensitive data in production.
❓ Frequently Asked Questions
Because different expense categories have completely different normal ranges — a $800 flight is routine, but a $800 office supplies purchase is suspicious. A single flat limit either lets clearly wrong claims through or rejects perfectly legitimate ones, depending on which category it was tuned for.
Replace a single If/Else with a Switch activity containing one branch per category, each checking the category name (using lower-case() for case-insensitive matching) and its own amount threshold, then mapping a response that includes which categoryLimit was applied.
Because an LLM-driven agent may send "Travel", "travel", or "TRAVEL" depending on phrasing. Wrapping the comparison in lower-case() makes the Switch condition case-insensitive, so the integration never breaks due to inconsistent casing from the AI Agent.
It should ask the user for the category before calling the tool at all, rather than guessing or defaulting silently. Without this rule, a missing category would fall through to the Otherwise branch and be evaluated against the wrong, overly conservative limit.
No. Only the backing integration version changes — the agent's LLM configuration, reasoning pattern, and endpoint stay exactly the same. Updating which integration version a tool points to is a one-click change in the tool editor.
📝 Quick Summary — What We Learned
-
Category-Based Thresholds →
Different expense types have different normal ranges.
A flat limit treats everything equally — which is exactly wrong for real-world expense policies. -
enum Validation →
Restricts
expenseCategoryto a known set of values. OIC validates the input before the integration even starts — protecting your Switch logic. - Multi-Branch Switch → One branch per category, each with its own amount check and response mapping. Like a post-office sorting machine — each parcel goes down its own chute.
- lower-case() for Robustness → Makes category matching case-insensitive so "Meals", "meals", and "MEALS" all work correctly. Defensive coding in the integration keeps the agent resilient against LLM variation.
- categoryLimit in Response → Gives the agent the exact rule that was applied so it can explain the decision clearly — not just "rejected" but "rejected because the Meals limit is $75."
- System Prompt — CATEGORY POLICY section → Lets the agent answer policy questions without calling the tool at all. Saves resources and makes the agent feel smarter and faster.
- Ask Before Act Rule → The agent must always confirm the category before calling the tool. An agent that waits for complete information is a trustworthy agent. 🤖
- 5 Test Scenarios → Approved within limit, rejected over limit, missing category (agent asks), exact boundary value, and policy question — all five must pass before going to production! ✅
Comments
Post a Comment