Module 6

Decision APIs

Language models are good at understanding requests and explaining results. Many business decisions, however, must be consistent, auditable, and sometimes provably optimal. Decision APIs let an agent hand those decisions to services built for them.

About 35 minutes4 Python examplesIntermediate

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

Kinds of decision services
KindHow it decidesTypical useExamples of technology
Rules and decision tablesExplicit if-then rules maintained by domain expertsEligibility, approvals, pricing policies, compliance checksThe OMG Decision Model and Notation (DMN) standard and engines that implement it, such as Drools
OptimizationA solver searches for the best solution that satisfies constraintsScheduling, routing, assignment, resource allocationGoogle OR-Tools, HiGHS, and commercial solvers
Predictive modelsA trained machine learning model returns a score or classRisk scoring, demand forecasting, anomaly detectionModels 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.

Illustrative example

The rules are invented for teaching and are not a real KFUPM policy.

Decision rules, the matching rule is highlighted
RuleWhenThenReason
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

Content in preparation

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

Check your understanding

An agent must assign 40 exam proctors to 25 rooms while respecting availability. What should decide the assignment?