What is a decision API?
A decision API is a service that receives structured facts and returns a decision, usually with a reason. Examples: approve or reject a request, choose a price or a zone, assign vehicles to routes, or schedule shifts. Inside, it might use business rules, an optimization solver, a predictive model, or a mix.
In an agentic system the work is split: the LLM understands and explains, and the decision service decides. The agent calls the service like any other tool.
Consistency
The same facts always produce the same decision, which a probabilistic model cannot guarantee.
Auditability
Every decision points to a rule or model version that experts own and regulators can review.
Optimality
Solvers find the best plan under constraints, which guessing by intuition cannot.
How an agent uses a decision service
Three common kinds of decision services
| Kind | How it decides | Typical use | Examples of technology |
|---|---|---|---|
| Rules and decision tables | Explicit if-then rules maintained by domain experts | Eligibility, approvals, pricing policies, compliance checks | The OMG Decision Model and Notation (DMN) standard and engines that implement it, such as Drools |
| Optimization | A solver searches for the best solution that satisfies constraints | Scheduling, routing, assignment, resource allocation | Google OR-Tools, HiGHS, and commercial solvers |
| Predictive models | A trained machine learning model returns a score or class | Risk scoring, demand forecasting, anomaly detection | Models served behind a REST API |
Interactive decision table
This table decides whether a lab equipment booking is approved automatically. It uses a first-hit policy: rules are checked from top to bottom, and the first rule whose conditions all match decides. Change the inputs and watch which rule fires.
The rules are invented for teaching and are not a real KFUPM policy.
| Rule | When | Then | Reason |
|---|---|---|---|
| Enable JavaScript to use the live table. The same rules are in the Python example below. | |||
"""Decision table: a tiny rules engine an agent can call
Many organizations encode business decisions as decision tables: each row is
a rule with conditions and an outcome. The OMG Decision Model and Notation
(DMN) standard formalizes this idea. A "first hit" policy returns the outcome
of the first rule whose conditions all match.
Why agents care: the LLM gathers and structures the inputs, but the decision
itself comes from an auditable, deterministic table that experts maintain.
Illustrative: a simplified decision table, not a full DMN implementation.
"""
# Decision: should a lab equipment request be approved automatically?
# Each condition is (field, operator, value). "-" in the output means "no rule".
RULES = [
{"id": "R1", "when": [("safety_training", "==", False)], "then": "reject", "why": "Safety training is required"},
{"id": "R2", "when": [("hazard_level", "==", "high")], "then": "manual_review", "why": "High-hazard equipment needs a supervisor"},
{"id": "R3", "when": [("hours", ">", 8)], "then": "manual_review", "why": "Long bookings need approval"},
{"id": "R4", "when": [("role", "in", ["faculty", "researcher"])], "then": "approve", "why": "Staff with training may book"},
{"id": "R5", "when": [("role", "==", "student"), ("hours", "<=", 4)], "then": "approve", "why": "Short student bookings are fine"},
{"id": "R6", "when": [], "then": "manual_review", "why": "Default rule"},
]
OPS = {
"==": lambda a, b: a == b, ">": lambda a, b: a > b, "<=": lambda a, b: a <= b,
"in": lambda a, b: a in b,
}
def decide(facts: dict) -> dict:
"""First-hit policy: return the first matching rule."""
for rule in RULES:
if all(OPS[op](facts[field], value) for field, op, value in rule["when"]):
return {"decision": rule["then"], "rule": rule["id"], "reason": rule["why"]}
return {"decision": "-", "rule": None, "reason": "no rule matched"}
requests = [
{"role": "student", "hours": 3, "hazard_level": "low", "safety_training": True},
{"role": "student", "hours": 6, "hazard_level": "low", "safety_training": True},
{"role": "faculty", "hours": 2, "hazard_level": "high", "safety_training": True},
{"role": "researcher", "hours": 2, "hazard_level": "low", "safety_training": False},
]
for facts in requests:
print(facts, "\n ->", decide(facts), "\n")
OpenAPI: describing decision services for agents
The OpenAPI Specification is a widely used, machine-readable format for describing HTTP APIs: their paths, operations, parameters, and JSON schemas. Because a model's tool definition also uses a name, a description, and a JSON schema, an OpenAPI operation maps naturally onto a tool. Several agent platforms can import an OpenAPI document and expose its operations as tools; custom GPT Actions in ChatGPT, for example, are defined with an OpenAPI schema. An OpenAPI-described service can also be wrapped as an MCP server (Module 5).
The example converts a small OpenAPI document for a parking-permit decision service into a tool, then lets a stand-in model call it. Input: the OpenAPI document and a user question. Output: the generated tool schema, the HTTP call, and the decision.
"""From an OpenAPI description to an agent tool
A decision service is often published as a web API and described with the
OpenAPI Specification: paths, operations, parameters, and JSON schemas.
Because the description is machine-readable, an agent framework can turn each
operation into a tool definition automatically, then call the API when the
model asks for it.
Illustrative: the API below is fake and is "called" in-process. In a real
system, call_operation() would send an HTTP request (for example with httpx).
"""
import json
# A minimal OpenAPI 3 document for a parking-permit decision service.
OPENAPI_DOC = {
"openapi": "3.1.0",
"info": {"title": "Campus Parking Decisions", "version": "1.0.0"},
"paths": {
"/decisions/parking-permit": {
"post": {
"operationId": "decideParkingPermit",
"summary": "Decide whether to grant a parking permit and which zone",
"requestBody": {"content": {"application/json": {"schema": {
"type": "object",
"properties": {
"role": {"type": "string", "enum": ["student", "staff", "visitor"]},
"distance_km": {"type": "number", "description": "Home-to-campus distance"},
"has_disability_permit": {"type": "boolean"},
},
"required": ["role", "distance_km"],
}}}},
}
}
},
}
def openapi_to_tools(doc: dict) -> list[dict]:
"""Convert each OpenAPI operation into a function-calling tool schema."""
tools = []
for path, methods in doc["paths"].items():
for method, op in methods.items():
schema = op["requestBody"]["content"]["application/json"]["schema"]
tools.append({"name": op["operationId"], "description": op["summary"],
"parameters": schema, "_route": (method.upper(), path)})
return tools
def fake_decision_service(body: dict) -> dict:
"""What the server would do (normally a rules engine or optimizer)."""
if body.get("has_disability_permit"):
return {"granted": True, "zone": "A (closest)", "rule": "accessibility"}
if body["role"] == "staff":
return {"granted": True, "zone": "B", "rule": "staff"}
if body["role"] == "student" and body["distance_km"] >= 10:
return {"granted": True, "zone": "C", "rule": "commuter-student"}
return {"granted": False, "zone": None, "rule": "students living within 10 km use the shuttle"}
def call_operation(tool: dict, arguments: dict) -> dict:
missing = [k for k in tool["parameters"].get("required", []) if k not in arguments]
if missing:
raise ValueError(f"missing required arguments {missing}")
method, path = tool["_route"]
print(f" HTTP {method} {path} {json.dumps(arguments)}")
return fake_decision_service(arguments)
tools = openapi_to_tools(OPENAPI_DOC)
print("Tool generated from the OpenAPI document:")
print(json.dumps({k: v for k, v in tools[0].items() if not k.startswith("_")}, indent=2))
# The model reads the tool schema and produces arguments from the user's words.
user = "I'm a student and I live 14 km from campus. Can I get a parking permit?"
model_tool_call = {"name": "decideParkingPermit", "arguments": {"role": "student", "distance_km": 14}}
print(f"\nUser: {user}\nModel calls: {model_tool_call['name']}")
result = call_operation(tools[0], model_tool_call["arguments"])
print("Decision service result:", result)
print("Agent reply: Yes. You qualify for a Zone C permit as a commuter student.")
Optimization as a decision API
Assigning three campus shuttles to three routes has only 6 possibilities, so we can check them all. The example shows that choosing "the best option for each shuttle, one at a time" (a greedy choice, similar to what a person or an LLM might do by intuition) can miss the true optimum. Real problems have millions of possibilities, which is why agents should call a solver.
"""Optimization as a decision API: assigning shuttles to routes
Some decisions are best made by an optimizer, not by an LLM: the model is
good at understanding the request, while a solver is good at finding the
provably best assignment under constraints.
Here an agent receives "assign 3 shuttles to 3 routes with the least total
travel time" and calls an optimization tool. With only 3! = 6 possibilities we
can check them all; real problems use solvers such as Google OR-Tools (see the
Colab notebook for the same problem solved with OR-Tools).
"""
from itertools import permutations
shuttles = ["S1", "S2", "S3"]
routes = ["North gate", "Dorms", "Library"]
# minutes for each shuttle to serve each route (rows: shuttles, cols: routes)
cost = [
[6, 7, 9],
[8, 11, 6],
[7, 9, 13],
]
def optimize_assignment(cost):
best_total, best_perm = float("inf"), None
for perm in permutations(range(len(routes))): # try every assignment
total = sum(cost[s][r] for s, r in enumerate(perm))
if total < best_total:
best_total, best_perm = total, perm
return best_total, best_perm
total, perm = optimize_assignment(cost)
print("Optimal assignment (minimum total minutes):")
for s, r in enumerate(perm):
print(f" {shuttles[s]} -> {routes[r]:<11} ({cost[s][r]} min)")
print(f"Total: {total} minutes")
# Compare with a "greedy" choice an LLM might make by intuition.
greedy, used = 0, set()
for s in range(3):
r = min((r for r in range(3) if r not in used), key=lambda r: cost[s][r])
used.add(r)
greedy += cost[s][r]
verdict = "worse than optimal" if greedy > total else "happens to be optimal here"
print(f"Greedy, one shuttle at a time: {greedy} minutes ({verdict})")
"""The same assignment problem with Google OR-Tools
Google OR-Tools is an open-source optimization suite. Its CP-SAT solver handles
assignment, scheduling, and routing problems. Wrapping a solver like this in a
function (or an MCP tool, or an OpenAPI service) lets an agent call it.
Run it in Google Colab or locally after installing ortools.
"""
# %%
# !pip -q install ortools
# %%
from ortools.sat.python import cp_model
shuttles = ["S1", "S2", "S3"]
routes = ["North gate", "Dorms", "Library"]
cost = [[6, 7, 9], [8, 11, 6], [7, 9, 13]]
def solve_assignment(cost):
model = cp_model.CpModel()
n = len(cost)
x = {(s, r): model.NewBoolVar(f"x_{s}_{r}") for s in range(n) for r in range(n)}
for s in range(n):
model.AddExactlyOne(x[s, r] for r in range(n)) # each shuttle gets one route
for r in range(n):
model.AddExactlyOne(x[s, r] for s in range(n)) # each route gets one shuttle
model.Minimize(sum(cost[s][r] * x[s, r] for s in range(n) for r in range(n)))
solver = cp_model.CpSolver()
status = solver.Solve(model)
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
return None
plan = [(shuttles[s], routes[r]) for s in range(n) for r in range(n) if solver.Value(x[s, r])]
return solver.ObjectiveValue(), plan
total, plan = solve_assignment(cost)
for s, r in plan:
print(f"{s} -> {r}")
print("Total minutes:", total)
Specific platforms: JEV and OpenAPI Decisions
This part of the module will cover two decision API offerings, JEV and OpenAPI Decisions. The course team is preparing verified descriptions, links, and examples. Until then, use the general concepts above: every decision service is called the same way, with structured facts in and a decision with a reason out.
Good practice
- Let the LLM extract and validate the facts, and ask the user when a required fact is missing.
- Never let the model invent the decision when a decision service exists. Instruct it to call the tool and report its result.
- Return a reason or rule identifier with every decision, and log inputs and outputs for audit.
- Version your rules and models, and evaluate the full agent again when they change (Module 10).