Objective
"Your model declined this application. Prove the decision record hasn't been edited since."
That question ends most AI-in-production conversations in regulated lending. The agent ran somewhere, the log went somewhere else, and the log is a mutable row in a table an operator can UPDATE. Under ECOA/FCRA you owe an adverse-action reason that provably matches what the system actually decided.
This recipe builds the answer as database mechanics, not policy:
- Guard rails — a governance envelope declared in DDL: the agent may never write, may never exceed N reasoning steps, and has a hard daily token budget.
- Traceability — every run is recorded automatically in
_system_agent_runs: what triggered it, when, what it decided, what it cost. - Tamper-evidence — those runs are sha256 hash-chained. Editing one in storage breaks its own hash and every downstream link.
- Durability — the trail is mirrored into a
CREATE IMMUTABLE TABLEledger the engine physically refuses toUPDATEorDELETE.
Requires: SynapCores v1.9.1+ (the decision-lineage columns on
_system_agent_runs). Every statement below was validated end-to-end on a stock v1.11.0-ce container — with no AI configuration at all, since CE ships a tool-capable default model (qwen2.5-coder:7b) and auto-pulls it on first boot.
Step 1: The application table
CREATE TABLE IF NOT EXISTS recipe_loan_applications (
app_id INTEGER PRIMARY KEY,
applicant TEXT,
amount_usd DOUBLE,
credit_score INTEGER,
dti_ratio DOUBLE,
employment_years DOUBLE,
submitted_at TIMESTAMP
);
Step 2: Create the agent — the guardrails ARE the DDL
Everything after WITH is the governance envelope. It is stored with the agent, enforced by the engine on every run, and readable back as data (Step 5) — not a prompt the model can talk its way around.
CREATE AGENT recipe_loan_reviewer
PERSONA 'aidb-assistant'
TASK 'Review the loan application in the activation row. Using execute_query on recipe_loan_applications, state a recommendation of APPROVE, REFER or DECLINE and the two facts that drove it. Answer in under 60 words.'
ON INSERT INTO recipe_loan_applications
WITH (
max_iterations = 3, -- hard cap on ReAct reasoning steps
allow_writes = FALSE, -- the agent CANNOT mutate any table
timeout_seconds = 120, -- per-run wall clock
budget_tokens_per_day = 200000, -- spend ceiling
on_budget_exhausted = 'pause' -- go quiet, don't fail open
);
Three of those deserve emphasis:
allow_writes = FALSEis the default and the only switch. There is no other route by which an agent's database tools may mutate a row. A recommender that cannot write cannot quietly approve a loan.ON INSERT INTOis AFTER-semantics. The agent observes committed rows; it cannot abort or alter yourINSERT.on_budget_exhausted = 'pause'fails closed. A runaway agent stops making decisions rather than making cheap ones.
Step 3: Submit applications — note how fast the write returns
INSERT INTO recipe_loan_applications VALUES
(9001,'Dana Whitfield', 45000.00, 712, 0.31, 6.0, '2026-07-20 09:15:00'),
(9002,'Marcus Ellery', 180000.00, 588, 0.54, 1.5, '2026-07-20 10:02:00'),
(9003,'Priya Raghavan', 22000.00, 780, 0.18, 11.0, '2026-07-20 11:40:00');
This returns in milliseconds. Event activation enqueues a job — the LLM loop never runs inside your transaction. Your write path keeps its latency profile no matter how slow the model is.
The three agent runs now execute in the background. Give them a moment (a local 7B model takes roughly 30–60 seconds per run; a hosted model is faster) before Step 4.
Step 4: Read the decision trail
SELECT agent_name, activation, started_at, status, tokens_in, tokens_out, output
FROM _system_agent_runs
ORDER BY started_at;
One row per decision, written by the engine — not by your application code, and not opt-in:
| agent_name | activation | status | output |
|---|---|---|---|
| recipe_loan_reviewer | INSERT ON recipe_loan_applications | success | APPROVE — Credit Score: 712 (above threshold); DTI Ratio: 0.31 (within range) |
| recipe_loan_reviewer | INSERT ON recipe_loan_applications | success | DECLINE — Credit Score: 588 (below threshold); DTI Ratio: 0.54 (above range) |
| recipe_loan_reviewer | INSERT ON recipe_loan_applications | success | APPROVE — Credit Score: 780 (above threshold); DTI Ratio: 0.18 (below range) |
activation is the provenance an auditor asks for first: what caused this decision to be made at all?
Step 5: Read back the guardrails as evidence
A regulator doesn't want your word that the agent couldn't write. They want the configuration.
DESCRIBE AGENT recipe_loan_reviewer;
Persona | aidb-assistant
Task | Review the loan application in the activation row...
Bindings | INSERT ON recipe_loan_applications
Enabled | true
max_iterations | 3
allow_writes | false
timeout_seconds | 120
budget_tokens_per_day | 200000
on_budget_exhausted | pause
memory | persistent
max_retries | 1
Created At | 2026-07-24T00:14:18Z
The governance envelope is a queryable database object with creation and modification timestamps. That is an auditable control, not a screenshot of a config file.
Step 6: The hash chain — this is what makes it tamper-evident
SELECT agent_name, prev_hash, entry_hash, verified
FROM _system_agent_runs
ORDER BY started_at;
recipe_loan_reviewer prev 000000000000… -> entry f80c31a854e0… verified: true
recipe_loan_reviewer prev f80c31a854e0… -> entry 4d05e1b280fa… verified: true
recipe_loan_reviewer prev 4d05e1b280fa… -> entry 24948964514b… verified: true
Each run's entry_hash is sha256(prev_hash ‖ the run's immutable content) — run id, agent, activation, timestamps, status, iterations, tokens, output. The first run links to a genesis value of 64 zeros. Every run after it links to the one before.
verified is not a stored flag. The engine recomputes the entire chain on read and reports whether each row still hashes to what it claims. To forge one decision undetectably you would have to rewrite the whole tail of the chain and the stored ledger head.
The chain is per tenant, not per agent — every agent's runs share one ledger. You cannot excise one agent's history without breaking everything recorded after it.
Step 7: The compliance control
One number, and the only acceptable value is zero:
SELECT COUNT(*) AS unverified_runs
FROM _system_agent_runs
WHERE verified = false;
unverified_runs
---------------
0
Run it on a schedule. Anything above zero means a decision record no longer matches its hash — investigate before you answer a single regulator question. (verified reads NULL for runs recorded before v1.9.1, which were never chained — honestly "not attestable" rather than a false green.)
Step 8: Pin the trail into an immutable ledger
The hash chain detects tampering. An immutable table prevents it, and survives independently of the agent runtime's own retention window.
CREATE IMMUTABLE TABLE recipe_decision_ledger (
run_id TEXT PRIMARY KEY,
agent_name TEXT,
activation TEXT,
decided_at TEXT,
status TEXT,
entry_hash TEXT,
decision TEXT
);
INSERT INTO recipe_decision_ledger
(run_id, agent_name, activation, decided_at, status, entry_hash, decision)
SELECT run_id, agent_name, activation, started_at, status, entry_hash, output
FROM _system_agent_runs;
Now try to cover one up:
UPDATE recipe_decision_ledger SET decision = 'APPROVE' WHERE status = 'success';
DELETE FROM recipe_decision_ledger WHERE status = 'success';
Error: Cannot UPDATE immutable table 'recipe_decision_ledger'. Immutable tables are append-only.
Error: Cannot DELETE from immutable table 'recipe_decision_ledger'. Immutable tables are append-only.
Not a permission you can grant yourself. Append-only is a property of the storage engine.
Step 9: The kill switch
Governance you can't exercise isn't governance. Stop the agent without destroying its history:
ALTER AGENT recipe_loan_reviewer DISABLE;
SHOW AGENTS;
name | persona | bindings | state | tokens_today
recipe_loan_reviewer | aidb-assistant | INSERT ON recipe_loan_applications | disabled | 450
Now submit another application:
INSERT INTO recipe_loan_applications VALUES
(9004,'Test Applicant', 99000.00, 640, 0.45, 2.0, '2026-07-21 08:00:00');
The row lands. No run is recorded — the binding is torn down, so no decision is made. Meanwhile tokens_today still shows the 450 tokens already spent: disabling an agent doesn't erase its accounting.
Even a full DROP AGENT retains the run history — the audit outlives the agent by design.
Cleanup
DROP AGENT IF EXISTS recipe_loan_reviewer;
DROP TABLE IF EXISTS recipe_loan_applications;
DROP TABLE IF EXISTS recipe_decision_ledger;
Expected Outcomes
- Step 3 returns in milliseconds — the agent runs off the write path.
- Step 4 shows one auto-written row per decision, with its triggering event.
- Step 6 shows a linked hash chain starting from 64 zeros, every row
verified = true. - Step 7 returns
0. - Step 8 rejects both
UPDATEandDELETEat the storage engine. - Step 9 shows a disabled agent that stops deciding but keeps its history and its token accounting.
Use it from your agent
- REST / SDK — every statement here runs through
POST /v1/query/execute. Your compliance dashboard is the Step-7 query on a timer; your adverse-action export is the Step-4 query filtered to a date range. - MCP — point any MCP client at
ws://<your-instance>/mcp?token=<jwt>and thequerytool reaches the same trail. An investigator's assistant can read the audit without a bespoke API. - Why in-database matters here — a stitched stack puts the decision in one system, the log in a second, and the tamper-evidence in a third (if it exists at all). Here the decision, its provenance, its governance envelope, its cost, and its cryptographic proof are rows in the same database, reachable by one
SELECT. And it runs entirely on your own infrastructure — a hosted agent platform cannot offer "provable and it never leaves your building."
Key Concepts Learned
CREATE AGENT … WITH (…)makes the governance envelope a database object, enforced by the engine and readable back withDESCRIBE AGENT— the difference between a guardrail and a polite instruction in a prompt.allow_writes = FALSEis the single switch that separates a recommender from an actor, and it is the default._system_agent_runsrecords every run automatically — you cannot forget to instrument an agent.prev_hash/entry_hash/verifiedmake the audit tamper-evident;CREATE IMMUTABLE TABLEmakes it tamper-resistant. Use both: detection plus prevention.- Event activation is AFTER-semantics — agents observe committed rows and never sit inside your write path.
Related recipes
- Prove an AI Agent's Decision Was Never Altered (Clinical Decision Support) — the same controls under HIPAA, where the agent must never touch the chart.
- Agentic FinServ Compliance Reviewer — the reasoning side of the same story: an agent that audits transactions against regulation.
- RAG with Citations — traceability for what an agent read, complementing this recipe's traceability for what it decided.