Execution Layer for Odoo
Verified execution.
What a verified write is, the Action Receipt it returns, and the guarantees around it: atomic execution, post-commit read-back, idempotency keys, all-or-nothing batches, and the truncation contract on reads.
A verified write is a write that is executed inside an atomic transaction, then read back from the database after commit and compared against the declared intent before success is ever reported. If what persisted does not match what was meant, the AI gets a structured failure, not a success.
The Action Receipt
Every write returns an Action Receipt: the record of the chain Executed, Persisted, Verified, Recorded, with the values that changed.
{ "receipt": { "receipt_id": "rcp_01J...", "audit_id": 12345, "tool": "update_records", "model": "project.task", "record_ids": [453], "executed": true, "persisted": true, "verified": true, "audited": true, "stored_records": [{ "id": 453, "user_ids": [7], "stage_id": [3, "In Progress"] }], "changed_fields": { "453": ["user_ids", "stage_id"] }, "assertions": { "only_if": "passed" }, "approval": { "request_id": null, "approved_by": null }, "idempotency": { "key": "...", "replay": false }, "context_layer": { "status": "none" }, "timing_ms": { "total": 84, "execute": 21, "readback": 6, "audit": 9 } }}The chain, in order:
executedthe operation ran inside a transaction.persistedthe explicit commit returned.verifieda fresh read after commit matched the declared intent and any assertions (relational fields are compared by id).auditedthe evidence row was written.
The JSON field
In prose and in the Execution Layer app the final step reads Recorded. In the receipt JSON
the field is audited. Same step, two names.
When verification fails
If a write commits but the stored value does not match the intent, for example an onchange or a
compute rewrote it, the layer returns VERIFICATION_FAILED. It does not report success and quietly
move on. The mismatch is audited and raises the default alert. This is the whole point: a success is
only a success once the database agrees.
Idempotency
Every write carries an idempotency key, scoped to the database, the user, and the tool. A timeout
followed by a retry with the same key and payload replays the original receipt, so the action runs
once, not twice. The same key with a different payload returns IDEMPOTENCY_CONFLICT.
Batches
A batch returns a batch_receipt:
{ "batch_receipt": { "mode": "atomic", "status": "committed" }}mode is atomic or best_effort; status is committed, rolled_back, or partial. In atomic
mode, the default, any single item failing rolls back the whole batch and returns BATCH_ITEM_FAILED
with the failing index. Either the whole batch persists, or none of it does.
Reads cannot pose as complete
Every bounded read carries a truncation contract, so a partial list can never be mistaken for the whole:
{ "count": 14, "total_count": 23, "truncated": true, "has_more": true, "next_offset": 14}The default read limit is 50, the maximum is 200, and an export can return up to 5000. An agent that
reads truncated: true, total_count: 23 knows to page through the rest before it answers.
Connect returns receipts too, honestly
On the free Connect tier, without nanti_mcp_verify,
you still get a receipt in the same shape, but with verified: false and
verification: "not_installed", and stored_records set to the in-transaction read-back.
Verification is a Complete feature, and the free tier says so plainly rather than pretending.
Next: governance.