A popular meme says an AI agent is just an LLM inside a while loop. Simplifying a lot, that's fair: the model gets an instruction, decides what to do, calls a tool, reads the result and goes around again. You can build one in about 20 minutes.

Getting it to work inside a real company can take months. Not because the loop is hard, but because the loop needs answers the company never wrote down.

This post is about one design decision that makes a big difference: keeping business policy out of the model and putting it in data and deterministic code the agent can only call.

The question the model should never answer

Here's a common case. A salesperson needs a 20% discount approved. Their limit is 15%. The manager is on vacation. Who approves?

In many companies nobody has a clear answer. The agent can query the CRM, run a flow or call an API. But if no one has defined who has authority over that discount, the model should not invent a commercial policy. Any answer it produces is a guess that looks like a decision.

So the first job is not prompt engineering. It's turning "who decides" into something explicit.

Put the policy in data, not in the prompt

An approval matrix is boring and that's the point. It can be a table, custom metadata, or a config file the business owns:

APPROVAL_MATRIX = [ # (max_discount_pct, role allowed to approve, backup role)  (15, "sales_rep", None), (25, "sales_manager", "regional_director"), (40, "regional_director", "cfo"), ] def approver_for(discount_pct, unavailable_roles): for max_pct, role, backup in APPROVAL_MATRIX: if discount_pct <= max_pct: if role not in unavailable_roles: return role if backup and backup not in unavailable_roles: return backup return None # nobody available: escalate, don't guess  return None # above any defined limit: out of policy 

The numbers above are placeholders. The real ones come from whoever owns the commercial policy. If that person doesn't exist yet, you've found the actual blocker, and it isn't technical.

Give the agent narrow tools with a gate

The agent loop stays simple. What changes is the tool layer: tools that change money, prices or contracts never execute directly. They return a proposal, and a deterministic gate decides what happens next.

def request_discount(opportunity_id, discount_pct, context): approver = approver_for(discount_pct, context["unavailable_roles"]) if approver is None: return { "status": "needs_human", "reason": "No approver defined or available for this discount", } return { "status": "pending_approval", "approver_role": approver, "opportunity_id": opportunity_id, "discount_pct": discount_pct, } TOOLS = { "get_opportunity": get_opportunity, # read only  "request_discount": request_discount, # proposes, never applies } def agent_loop(llm, message, context, max_steps=10): for _ in range(max_steps): reply, tool_calls = llm(message, tools=list(TOOLS)) if not tool_calls: return reply results = [] for call in tool_calls: if call.name not in TOOLS: results.append({"error": f"tool {call.name} not allowed"}) continue results.append(TOOLS[call.name](**call.args, context=context)) message = results return {"status": "needs_human", "reason": "step limit reached"} 

A few things that are easy to skip in a demo:

  • A step limit. while True is fine in a meme. In production, the loop needs a budget and a defined way out.
  • An allowlist. The model can only call tools you registered, and each tool has the smallest scope that works.
  • "needs_human" as a first-class result. Stopping is a valid outcome, not a failure. The agent should say why it stopped and who should look at it.
  • Read tools and write tools are different things. Reading an opportunity is cheap to get wrong. Changing a price list linked to active contracts is not.

Not everything needs an agent

If a decision fits in an if, a rule engine, a flow or plain code will do it faster, cheaper and the same way every time. In Salesforce, that's still Flow and Apex territory. Agents earn their place where you need interpretation: free text, messy context, choosing between paths you can't enumerate.

Mixing the two up is expensive in both directions. Use reasoning where you need reasoning. Use rules where you need the system to do exactly what it should.

The takeaway

Building the engine of an agent can be surprisingly simple. The hard part starts when you connect it to a real company: permissions, duplicate data, legacy processes and the exceptions that never show up in demos.

Solve the process first. Then automate it.

I wrote the business side of this, with a checklist of what to review before letting an agent work on real Salesforce data, on EGA Futura Discover (in Spanish): Por qué un agente de IA que se arma en 20 minutos tarda meses en funcionar dentro de Salesforce.

Juan Manuel Garrido, founder of EGA Futura and co-founder of Vantegrate.