Skip to content
Expedify
Building Production-Ready AI Agents

Module · M2 · Ground the decision

Let it read the real account

Lesson 7 of 21 · 8 min

The policy is now retrievable, and it is still undecidable. Read it again and notice what it needs: an amount, a consumption percentage, a count of certificates issued, and a purchase date. None of those are in the policy. None of them are in the customer’s message either — the customer said “half our team” and “a week”.

So the agent has to establish the facts of the case itself. That means giving it a way to read the account — and this is the point in the build where the obvious approach does not work.

What does not work, and why it is worth knowing

The natural choice is the CRM node — CRM Manager, pinned to search deals — attached to the agent as a tool. Attached that way, with its configuration properly locked down, it was called and it returned:

Verified against the seeded organisation. The node reports success every time.

data: {"name": {"contains": "Nimbus"}}

What came back
all 90 deals in the organisation

filters: {"name": {"contains": "Nimbus"}}

What came back
all 90 deals in the organisation

the search field itself

What came back
not offered to the agent at all — it is in the node’s configuration schema and not its tool schema, so marking it agent-fillable produced a tool with zero parameters

This is the failure shape to internalise, not the node. Nothing errored. A tool returned a large, well-formed, entirely unfiltered result, and an agent downstream of it will happily reason over the first row. In lesson 1 that is exactly what happened: it searched, took the first result, and moved ₹1,20,000 onto a stranger’s account. A filter that silently does not filter is worse than one that fails.

What works: the agent picks the argument, never the query

Replace the entity search with a database read whose SQL is fixed and whose only variable is one named slot the agent fills:

the account tool's query, verbatim from the build
SELECT d.name, d.value, d.currency, d.stage_name,
       d.custom_fields->>'seats'                   AS seats,
       d.custom_fields->>'consumption_avg'         AS consumption_avg,
       d.custom_fields->>'certificates_issued'     AS certificates_issued,
       d.custom_fields->>'outage_days'             AS outage_days,
       d.custom_fields->>'refund_requested_amount' AS refund_requested_amount
FROM deals d
WHERE d.name ILIKE '%' || {{ai:account}} || '%'
ORDER BY d.value DESC
One slot. The agent decides which account; it never decides what is selected, from where, or with what limit.

The account tool as configured

Read-only by configuration, and pinned so the agent cannot rewrite the statement.

Loop BodyCompleteNoYes

Scroll for all 12 steps →

On the verified run the agent called it with {"account": "Nimbus"} and got back one row:

Nobody put these in the prompt. The agent looked, and then quoted them back in its proposal.

value ₹1,20,000

What the policy needed it for
the ₹50,000 authority limit — this is more than twice it

10 seats

What the policy needed it for
a corporate purchase, so corporate terms apply

consumption_avg 57

What the policy needed it for
past the 50% line in the refund windows

certificates_issued 3

What the policy needed it for
the clause that extinguishes refund eligibility outright

outage_days 6

What the policy needed it for
the service failure is real, and the sanctioned remedy is not cash

The principle, which generalises well beyond this node. Let the model choose the ARGUMENT and never the QUERY. A wrong argument returns zero rows and fails loudly, in front of you. A model that writes its own query fails quietly, with a result that looks like an answer — and in one run of this build, an agent handed a fully pinned query replaced the entire statement with invented SQL against tables that do not exist. Lesson 9 is about why it could.

Pin the mode as well as the statement. The database node is write_internal — the tier is what the node can do, not what your SQL happens to say today. Setting the mode to read is what keeps a SELECT a SELECT.