Unit 10.01: Building it with guardrails from the start
Guardrails added afterwards live in prompts. Guardrails built in from the start live in the tool.
The checks inside the capability
The payment tool refuses an unapproved payment, an over-ceiling payment and a non-positive amount - before doing anything.
The code defines the tool and calls it with three cases.
from crewai import Agent
from crewai.tools import tool
MAX_PAYMENT = 5_000.0
@tool("post_payment")
def post_payment(invoice_id: str, amount: float, approved_by: str) -> str:
"""Post an approved payment. Refuses anything unapproved or over the ceiling."""
if not approved_by:
return "REFUSED: no approver recorded"
if amount > MAX_PAYMENT:
return f"REFUSED: {amount} exceeds the {MAX_PAYMENT} ceiling"
if amount <= 0:
return "REFUSED: amount must be positive"
return f"posted {amount} for {invoice_id}, approved by {approved_by}"
poster = Agent(role="Payment poster", goal="Post approved payments only",
backstory="You post what has been approved. You approve nothing.",
tools=[post_payment], allow_delegation=False)
CASES = [
{"invoice_id": "INV-1", "amount": 240.0, "approved_by": "finance-mgr-2"},
{"invoice_id": "INV-2", "amount": 240.0, "approved_by": ""},
{"invoice_id": "INV-3", "amount": 90_000.0, "approved_by": "finance-mgr-2"},
]
for args in CASES:
print(f"{args['invoice_id']}: {post_payment.run(**args)}")
# The guardrails are inside the tool, so they hold regardless of what the agent
# was persuaded to attempt. Built afterwards they would live in a prompt.
All three checks are inside the function, so they hold regardless of what the agent was asked, persuaded, or instructed. approved_by being required at the tool boundary means an approval cannot be skipped by any path that reaches the tool.
Compare with the retrofitted version: an agent told in its backstory not to post over five thousand. That instruction is read by a model, competes with everything else in the context, and cannot be shown to an auditor.
The mistake this prevents
The mistake is building the workflow first and adding guardrails when it works. By then the tool signature has no approved_by parameter, adding one means changing every call site, and the path of least resistance is a line in a prompt.
Takeaway
Put the guardrails inside the tool, including requiring the approver as a parameter. Built in from the start they constrain every path; added later they end up in a prompt.
