An audit trail has one job: to be right when someone needs it. Not usually right, not right on quiet days. Right when an auditor, a customer, or your own engineers open it after an incident and rely on every line.
Reliability problems in agent logging are rarely dramatic. They are small and quiet. A network call times out, your code retries, and the same event is written twice. A worker crashes between an action and its log entry, and the action is never recorded. A busy minute produces a burst of events and some are dropped. Each one is easy to dismiss, and each one weakens the evidence. A duplicate makes a session look like the agent acted twice. A gap makes the chain of reasons end in the middle of nowhere.
This guide shows how to make logging dependable using ZizkaDB, an open source causal database for AI agents. We cover batching for volume, idempotency keys for safe retries, and checks that tell you whether the record is complete. I describe features as documented in the project changelog and SDK documentation at the time of writing. The code is illustrative, so confirm exact parameter names in the current SDK reference.
Why this matters to the business
Teams often treat logging reliability as a technical nicety. It is a business risk, for three reasons.
Evidence loses value when it can be questioned. If a customer disputes what your agent did and your record contains an unexplained duplicate or gap, the argument moves from what happened to whether your record can be trusted. A clean, complete record ends that argument quickly.
Compliance expects completeness. Rules such as the EU AI Act describe automatic recording of events over a system's lifetime for high-risk systems. A record with holes is hard to defend as automatic and complete. Check with legal counsel which obligations apply to you.
Gaps break debugging. Causal lineage works by following links from an event back to its cause. If the cause was never recorded, the chain stops, and an engineer is back to guessing.
The four ways an audit trail goes wrong
It helps to name the failures, because each has a different fix.
Duplicates. A retry after a timeout writes the same event twice, even though the first write succeeded.
Gaps. A crash, a deploy, or a dropped request means an action happened but was never logged.
Misordering. Events from parallel work arrive out of order, so a cause appears to follow its effect.
Overload. A burst of activity overwhelms a one-event-per-call approach, adding latency to the agent itself or losing events.
The sections below address each.
Step 1: Decide what a failed log call should do
Before writing code, make one design decision: what should happen if logging fails?
There are two reasonable policies, and the right one depends on the agent's job.
Fail open. The agent keeps working, and the failed log is queued for retry. This suits low-risk assistants where availability matters more than a perfect record.
Fail closed. The agent pauses or refuses to take the action until the log is written. This suits high-risk actions, such as approving a payment or changing a customer record, where an unrecorded action is unacceptable.
Many teams mix the two: fail open for reads and chat, fail closed for actions with real consequences. Write the policy down, because an auditor will ask for it.
Step 2: Use idempotency keys so retries are safe
An idempotency key is a unique label that says: this is the same event as before. When the server sees a second write with a key it has already stored, it does not create a duplicate. That makes retries safe, which means you can retry aggressively without polluting the record.
ZizkaDB supports idempotency keys according to its changelog. The pattern looks like this:
python
event_key = f"{session_id}:{step_number}"
await db.log(
agent="support-agent",
event="refund_approved",
data={"order_id": "A-1042", "amount": 40},
idempotency_key=event_key,
)
The parameter name above is illustrative, so check the SDK reference for the exact spelling.
The important part is how you build the key. It must be the same on every retry of the same event, and different for different events. Good keys combine stable facts, such as the session identifier and the step number. Bad keys use a random value generated fresh on each attempt, which defeats the purpose, or a timestamp, which changes on every retry.
With keys in place, your retry logic becomes simple and safe: if a write times out, send it again with the same key, and stop worrying about whether the first attempt secretly succeeded.
Step 3: Batch events for volume
Writing one event per network call is fine for a demo. In production, an agent can produce dozens of events per task, and each call adds latency and failure points.
The SDK includes a batch logging function for this. Instead of many small writes, you collect events and send them together:
python
await db.log_batch([
{"agent": "support-agent", "event": "tool_called", "data": {"tool": "lookup_order"}},
{"agent": "support-agent", "event": "tool_result", "data": {"status": "ok"}},
{"agent": "support-agent", "event": "reply_drafted", "data": {"chars": 420}},
])
Batching helps in three ways. It cuts the number of round trips, so the agent is slower less often. It reduces the chance of partial failure across many small calls. And it keeps related events together, which helps preserve their order.
Be thoughtful about batch size and timing. A very large batch that fails must be retried as a whole, and a batch held too long risks being lost if the process dies. A common compromise is to flush on a small count, such as every few dozen events, or after a short time, such as a second or two, whichever comes first. Combine batching with idempotency keys on each event, so a retried batch cannot create duplicates.
Step 4: Keep causal links and order intact
ZizkaDB links each event to its cause, and events can chain automatically to the previous one. Under parallel work, two things can go wrong: a child event arrives before its parent, or a sequence of events is written in a different order than it happened.
Three habits protect you:
Write parents first. When a step depends on an earlier event, log the earlier one and capture its identifier before logging the later one.
Pass the parent explicitly in parallel code. Do not rely on the automatic chain when many tasks run at once, because the chain follows the last event written and parallel tasks will interleave.
Give each unit of work its own session. A session per task or per request keeps parallel work from tangling.
If you use the framework integrations for LangChain, LangGraph, or CrewAI, they handle much of this for you, but check how they behave under parallel branches before relying on them.
Step 5: Protect against crashes between action and record
The hardest gap to close is the one between doing something and recording it. If the agent sends a refund and the process dies before the log call, the action happened with no record.
The safest order depends on your policy:
Log intent first, then act, then log the result. If a crash happens midway, you have an intent event with no result event, and that mismatch is visible and easy to find.
Make the action itself idempotent too. Use a key on the outgoing action, so a restart can safely repeat the step without refunding twice.
An intent without a result is much better than silence. It turns an invisible gap into a queryable one.
Step 6: Check that the record is complete
Reliability is not finished when the code is written. You need to check, regularly, that the record is whole.
Verify the chain. ZizkaDB stores a SHA-256 checksum for each event and a chain hash linking each event to the previous one for that agent. The verification call checks the whole chain and returns whether it is intact, a summary, any problems found, and a chain head:
python
report = await db.verify_chain("support-agent")
print(report)
Run it on a schedule. The ZizkaDB Audit action on GitHub can do this weekly against your live instance and produce a report attested by GitHub. Store the chain head somewhere independent of the database.
Know what verification does and does not catch. It shows that stored records were not edited, inserted, deleted, or reordered after writing. It cannot tell you about events that were never written in the first place. A missing event leaves no break in the chain, because nothing was there to break it.
Reconcile against another source. To find unwritten events, compare counts with something independent. For example, compare the number of refunds in your payments system with the number of refund events in the record for the same day. A mismatch tells you where to look.
Look for orphan and unfinished events. Search for intent events with no result, and for events whose parent cannot be found. Both point to gaps.
Step 7: Monitor your own logging
Treat the logging path as a system with its own health signals. Track at least:
the number of failed log calls and retries per hour,
the age of the oldest queued, unsent event,
the count of events written per agent per day, compared with the usual range,
the result of each scheduled chain verification.
A sudden drop in daily event counts is often the first sign that something stopped logging. Alert on it. Treat a verification run that records zero events as a failure, not a pass.
Common mistakes to avoid
Random idempotency keys. A key that changes on every retry prevents nothing. Build it from stable facts.
Retrying without keys. This is how duplicates arrive. Add keys before adding retries.
Giant batches with no flush rule. A batch that waits too long can vanish with the process.
Relying on the automatic chain in parallel code. Pass the parent explicitly.
Logging only after the action. Log intent first for actions with consequences.
Assuming verification proves completeness. It proves nothing was altered, not that nothing is missing.
Having no written failure policy. Decide in advance what happens when logging fails.
What it saves
These figures are modeled assumptions, not measured results, so replace them with your own. Suppose an incident review or customer dispute happens ten times a month. In a messy record, three of them stall because of duplicates, gaps, or an unexplained order, and each stalled case costs about six extra engineering hours at 100 euro an hour, which is 1,800 euro a month. A reliable record removes most of that. The setup is a few days of engineering. The larger value is trust: a record that nobody can dispute is a record that customers and auditors accept without a fight.
Start this week
Pick one agent and answer three questions. What happens today when a log call fails? Can a retry write the same event twice? Could you tell if a day of events were missing? If any answer is unclear, add stable idempotency keys, move to batched writes with a flush rule, log intent before consequential actions, and schedule a weekly chain verification with a daily event count check.
Want to test ZizkaDB on your agent? Try our open source version here: https://github.com/ZIZKA-AI-SL/ZizkaDB
Interested in a design partnership? Fill in the form on our site or reach me directly at founder@zizka.ai
Sources: ZizkaDB changelog · ZizkaDB Python SDK wiki · ZizkaDB Audit on GitHub Marketplace · AI Act Article 12 · ZizkaDB repository