Let it read the real account
The policy is undecidable without the record. Give the agent the argument and never the query.
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:
| What was asked for | What came back |
|---|---|
data: {"name": {"contains": "Nimbus"}} | all 90 deals in the organisation |
filters: {"name": {"contains": "Nimbus"}} | all 90 deals in the organisation |
| the search field itself | 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 |
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:
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 DESCThe account tool as configured
Read-only by configuration, and pinned so the agent cannot rewrite the statement.
Scroll for all 12 steps →
On the verified run the agent called it with {"account": "Nimbus"} and got back one row:
| What the record said | What the policy needed it for |
|---|---|
| value ₹1,20,000 | the ₹50,000 authority limit — this is more than twice it |
| 10 seats | a corporate purchase, so corporate terms apply |
| consumption_avg 57 | past the 50% line in the refund windows |
| certificates_issued 3 | the clause that extinguishes refund eligibility outright |
| outage_days 6 | the service failure is real, and the sanctioned remedy is not cash |
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.
Related lessons
Base rates — what a piece of evidence is actually worth
A face-recognition system that is 99.9% accurate and almost entirely wrong, and a number that sent an innocent woman to prison. Both are the same arithmetic, and it is the arithmetic that decides what any piece of evidence is worth.
ReadConfirmation and survivorship — what you never looked for
Two questions about evidence you did not go looking for. One is a rule you have to discover, and one is a pattern in five famous people — and in both, the thing that would have told you the truth is the thing nobody checks.
ReadLoss aversion, sunk cost and regression — what it costs you
Four questions you answer about yourself rather than about a scenario, and your own answers are the finding. Then the pattern that makes praise look useless and criticism look like it works, whatever you actually do.
Read
