Storage Adapters
A storage adapter is the backend that holds your audit trail — the durable home for every decision Briefcase AI captures, and the surface you query when someone asks why a decision was made.
storage For: platform & governance
How it works
-
Init — call
briefcase.init()once and construct a backend. -
Create — build a
DecisionSnapshotfor eachclassify_ticketcall. -
Save —
save_decision()persists the record and returns its id. -
Query — pull records back with a
SnapshotQuerywhen you need to review them.
flowchart LR
A[classify_ticket] --> B[DecisionSnapshot]
B --> C[SqliteBackend]
C --> D[(Audit trail)]
E[Reviewer query] --> C
Install
pip install briefcase-ai[storage]Which backend should I use?
| Backend | Persistence | Reach for it when |
|---|---|---|
SqliteBackend.in_memory() | No — gone on exit | Tests and local experiments, where no durable audit trail is needed |
SqliteBackend("path.db") | Yes — a file on disk | A single node where you want a real, queryable audit trail |
BufferedBackend | Yes — wraps another backend, after a flush | High write volume, where one transaction per batch beats one per decision |
BufferedBackend is not a separate store. It wraps a durable backend (such as SqliteBackend) and holds decisions in memory until buffer_size of them accumulate, then writes the whole batch in one transaction. save_decision returns the decision’s id straight away, and load_decision finds a decision that is still in the buffer, so a returned id is usable immediately.
Init -> create -> save -> query
-
Init the runtime and backend
import briefcasefrom briefcase.storage import SqliteBackendbriefcase.init() # start the native runtime once per processbackend = SqliteBackend("decisions.db") -
Create a decision for each
classify_ticketcall.from briefcase import DecisionSnapshot, Input, Outputdecision = DecisionSnapshot("classify_ticket")decision.add_input(Input("text", "reset my password", "string"))output = Output("category", "account_access", "string")output.with_confidence(0.92)decision.add_output(output)decision.add_tag("queue", "support") -
Save the record —
save_decision()returns its id.decision_id = backend.save_decision(decision)loaded = backend.load_decision(decision_id)print(loaded.function_name) # classify_ticket -
Query the audit trail with a
SnapshotQuery.from briefcase import SnapshotQueryresults = backend.query(SnapshotQuery().with_function_name("classify_ticket").with_tag("queue", "support"))print(len(results))
briefcase.init() must be called once before using a backend to start the native runtime.
Backends in detail
In-memory (for tests)
SqliteBackend.in_memory() keeps data in memory — fast and ephemeral, the right choice for tests where you do not need records to survive the process.
import briefcasefrom briefcase.storage import SqliteBackend
briefcase.init()backend = SqliteBackend.in_memory()File on disk (for a real audit trail)
SqliteBackend(path) writes to a file — a durable, queryable audit trail in one place, the workhorse for single-node deployments.
import briefcasefrom briefcase.storage import SqliteBackend
briefcase.init()backend = SqliteBackend("decisions.db")print(backend.health_check()) # TrueBuffered (for high volume)
BufferedBackend wraps a durable backend and commits decisions in batches. Use it as a context manager so a partial batch cannot be left behind:
import briefcasefrom briefcase.storage import SqliteBackend, BufferedBackendfrom briefcase import DecisionSnapshot, Input
briefcase.init()store = SqliteBackend("decisions.db")
with BufferedBackend(store, buffer_size=100) as backend: decision = DecisionSnapshot("classify_ticket") decision.add_input(Input("text", "update my address", "string")) decision_id = backend.save_decision(decision)
print(backend.pending()) # 1 — not written yet print(backend.load_decision(decision_id).function_name) # readable anyway
# leaving the block flushes; the batch is now durableflush() writes the batch immediately and returns how many decisions it wrote. pending() is how many are waiting.
A governance query: load decisions for review
The point of a durable backend is the review it enables. When a reviewer asks for last week’s support-queue decisions, you answer with a tagged SnapshotQuery against the same store that captured them.
import briefcasefrom briefcase.storage import SqliteBackendfrom briefcase import SnapshotQuery
briefcase.init()backend = SqliteBackend("decisions.db")
# Pull every support-queue triage decision for reviewquery = ( SnapshotQuery() .with_function_name("classify_ticket") .with_tag("queue", "support") .with_limit(50) .with_offset(0))for decision in backend.query(query): # hand each record to a reviewer, or replay it to verify ...SnapshotQuery supports with_function_name, with_module_name, with_tag, with_limit, and with_offset. There is no time filter, so tag with what you will want to select on later. query() returns Snapshot objects; the records are on .decisions:
for snapshot in backend.query(query): for decision in snapshot.decisions: print(decision.function_name, decision.outputs[0].value)From here a reviewer can audit a decision or replay it.
Snapshots: grouping multiple decisions
A Snapshot groups several decisions; save() returns the snapshot id and load() returns it.
import briefcasefrom briefcase.storage import SqliteBackendfrom briefcase import DecisionSnapshot, Input, Snapshot
briefcase.init()backend = SqliteBackend.in_memory()
decision = DecisionSnapshot("classify_ticket")decision.add_input(Input("text", "where is my order", "string"))
session = Snapshot("session")session.add_decision(decision)
snapshot_id = backend.save(session)restored = backend.load(snapshot_id)print(len(restored.decisions)) # 1The persistence interface
SqliteBackend exposes the full interface (BufferedBackend only buffers save_decision calls before flushing them to the backend it wraps):
backend.save(snapshot) # store a Snapshot, returns its idbackend.load(snapshot_id) # load a Snapshotbackend.save_decision(decision) # store a DecisionSnapshot, returns its idbackend.load_decision(decision_id)backend.query(snapshot_query) # run a SnapshotQuerybackend.delete(snapshot_id)backend.health_check()Available backends
| Backend | Class | Description |
|---|---|---|
| SQLite | SqliteBackend | Local SQLite database (file or in-memory) |
| Buffered | BufferedBackend | Wraps a backend and batches writes |
Limits
Queries filter on identity and tags, never on time. SnapshotQuery offers
function name, module name, tag, limit, and offset. “Every decision from last week”
is not expressible against the store; tag the window you will want (add_tag("day", "2026-08-13")) at write time, or filter in Python after loading.
query() returns Snapshot objects, not decisions. Each result wraps its
records in .decisions. Iterating the query result directly gives you snapshots.
Buffered decisions live in memory until they are flushed. A process that dies
with a partial batch loses it, and query() cannot see pending decisions (though
load_decision can). Use the context manager, call flush() before exit, or set
buffer_size=1 to write through. Dropping a backend with unflushed decisions prints
a warning to stderr rather than losing them quietly.
A missing id raises rather than returning None. load_decision and load
raise KeyError. delete is the exception: it returns False for an id that was
not there.
One process per file. Opening a .db that another process already holds fails
at construction, not at write time: RuntimeError: Failed to create SQLite backend: Connection error: Failed to set WAL mode: database is locked. Give each process its
own file, or put the shared store behind one service.
Nothing expires. There is no retention policy, TTL, or vacuum. The file grows until you prune it yourself, and records you delete are gone with no tombstone. If you need history that survives correction, that is Bitemporal Storage.
API reference
briefcase.storage has the full backend and
SnapshotQuery surface.
Where this fits
Storage is the Store & Query act: the durable home for everything Capture produced, and the surface the later acts read from.