Imagine a wealth management firm whose agent takes withdrawal requests. One day someone changes a client's destination account and asks for a withdrawal minutes later, and it turns out to be fraud. Compliance sets a new rule: after a destination change, no withdrawal goes through until the back office has verified the change with the client. How long the rule applies depends on the account's policy.
This is more than a value to update. The firm never stored when a destination changed or when it was verified, and it has no policies to read. If the agent's rules live in its code, this means writing that logic wherever the agent can request a withdrawal, then releasing a new version. If they live in the prompt, you have asked the model to follow the rule, and nothing makes it.
Before the rule applies, someone should be able to answer three questions:
- Which past requests would it have refused?
- Who agreed to it?
- Afterwards, which rule refused each request, and in which version?
The rest of this piece makes that change in Zarel, on a demo contract in a local stack, on 3 November 2026. The commands and outputs are from that run, trimmed. It is the same demo agent that refuses to give investment advice on our site, extended to record withdrawal requests. It moves no money.
Step 1: add the data
In Zarel, an agent's rules and the data they read are declared in a contract, outside its code. The first publication adds a policy, and the two dates on each account:
- name: withdrawal_policies
fields:
- name: name
type: string
required: true
- name: destination_hold_days
type: number
required: true
- name: accounts
fields:
# the fields it already had, and:
- name: policy
type: reference
references: withdrawal_policies
- name: destination_changed_at
type: datetime
- name: destination_verified_at
type: datetime
- name: destination_hold_days
type: number
compute:
expression: policy.destination_hold_days
strategy: read
No migration, no deploy, and no request is treated differently. The back office fills in the policies and the accounts while requests keep arriving. In this run, five requests for 30,000 were accepted.
The data goes first for a reason: the next step judges past requests, and a request made before these fields existed cannot say what they held.
Step 2: try the rule against what already happened
The rule is a check on each withdrawal request:
- name: recent_destination_change
on: [ create ]
effect: deny
expression: >-
account.destination_changed_at > now() - duration(account.destination_hold_days, 0, 0, 0)
and (account.destination_verified_at == null
or account.destination_verified_at < account.destination_changed_at)
message: recent_destination_change
A dry run judges the five past requests against it:
$ zarel contract spec dry-run rule.yaml --from ... --to ... --watch
- Sessions evaluated: 5
- Re-evaluated with the same outcome: 3 (60.0%)
- Outcomes changed: 2 (40.0%)
| from | to | count | cause |
| executed | refused | 2 | Diverged stages: checks; check:withdrawal_requests.recent_destination_change |
Each request, under the rule:
- Refused: destination changed 2 days ago, 7-day hold, not verified.
- Refused: destination changed 2 days ago, 7-day hold, verified before that change.
- Accepted: destination changed 60 days ago, 7-day hold.
- Accepted: destination changed 2 days ago, 7-day hold, verified after that change.
- Accepted: destination changed 2 days ago, 1-day hold.
A dry run over a longer window also reaches requests from before the new fields existed. It does not report those as unchanged. It reports that it could not judge them (field_not_declared_then). It also does not run the model again, so it cannot say what the agent would have proposed under the new rule.
Step 3: someone else approves it
In this run the contract names who approves a change to its entities, so publishing creates a proposal and nothing of it applies yet:
$ zarel contract spec apply rule.yaml
"publication": {
"status": "proposed",
"requirements": { "entities": "four_eyes" }
}
While it waited, a new request for the first of those accounts was still accepted, and an advisor who tried to approve the proposal was refused.
The proposal also told the approver something they need to know: one account has no policy yet, so the rule cannot apply to it until someone sets one. A check over an empty value does not fire, and it is better to learn that before the rule goes live.
The back office user approved it through the API. The publication recorded who proposed it, who approved it and in which role. This step is optional: a contract that names no approvers goes live when it is published.
Step 4: it applies from the next action
No deploy happened. That first account's next request:
HTTP 422 validation_violation
a validation rule refused the write (entity withdrawal_requests; rule recent_destination_change)
The three accounts accepted above were accepted again. Refusals from before and after a change cite different contract versions by their hash, in the chain of decisions.
Two more things changed with no new version of the contract, because they are data. The back office shortened the policy to 1 day and the same request was accepted. With the hold back at 7 days, an advisor tried to record the verification and was refused. The back office recorded it, and the request went through.
What this does not do
- It covers what goes through Zarel. Anything the agent can do some other way is not judged by any of this.
- Someone still edits a contract and publishes it. Here that meant a YAML file and a terminal, and the approval went through the API. A risk officer would not work that way.
- Publishing a whole document requires update permission on every section in it, even when only one changed. Someone who should only touch this rule would use a draft, which this run did not cover.
- The rule refuses the request. It does not hold it for someone else to decide: a check that asks for confirmation asks the person acting. Holding it would need a state on the request, which this run did not cover.
- The requests went straight to the API, with no model involved, on one machine with no load.
- Zarel is pre-launch, with no customers in production and no certifications. Whether changing a rule is what hurts most for teams running agents is something we are still asking them.