What Studio records
Turning it on
There are three switches. A run checks them when it starts, so an agent created before you turned Studio on is still recorded.
| Switch | Use it for |
|---|---|
LOOMFLOW_STUDIO=1 (or a file path, or off) | No code change: record a script, a server, a test run. LOOMFLOW_STUDIO_PROJECT names the project and LOOMFLOW_STUDIO_KEEP_DAYS deletes old runs. |
loomflow.studio.enable(...) | Turning it on in code, with options. Wins over the environment variable. |
with loomflow.studio.session(...): | Changing the project, tags or metadata for the runs started inside the block, or enabled=False to record none of them (True records them even when Studio is otherwise off). Wins over both. Blocks nest. |
import loomflow.studio
loomflow.studio.enable(
".loomflow/studio.db", # the default
project="support-bot", # default: the current folder's name
sample_rate=1.0, # the fraction of traces to keep
capture_content=True, # False: timing, tokens and structure only
redact="pii", # or a str -> str function
keep_days=30, # delete runs older than this; default: keep all
)
with loomflow.studio.session(tags=["nightly"], metadata={"build": "812"}):
... # runs here carry the tags and metadata
with loomflow.studio.session(enabled=False):
... # runs here aren't recordedloomflow.studio.disable() stops recording runs started after it (the
environment variable still applies), and loomflow.studio.is_enabled()
tells you whether a run started now would be recorded. Set
LOOMFLOW_STUDIO=off in your test environment so a variable exported
in your shell doesn’t fill the file during test runs.
What a trace holds
A trace is one run, as a tree of steps:
| Step | What Studio keeps |
|---|---|
| Agent run | Agent name and version, user, session, the prompt and the final output (and parsed output), turns, tokens, cost, how the run ended. |
| Turn | Timing, and the model and tool calls inside it. |
| Model call | The model, the messages sent (the most recent 60), the reply and any tool calls it asked for, input, output and cached tokens, cost, finish reason. |
| Tool call | Arguments, result or error, whether it was denied, the permission decision (allow, deny, ask) and, for ask, whether the approver approved it and whether they edited the arguments. |
| Workflow run and step | Each node’s output, and the agents that ran inside it. |
| Event | Guardrail hits (which guard, what it did), architecture progress (a debate round, a reflexion lesson, a router’s choice), budget warnings, errors. |
Every model call is recorded, not only those in the ReAct loop: planners, critics, judges and output-validation retries in the other architectures appear as model calls too.
Runs started inside a run join its trace instead of starting their own.
A sub-agent called from a tool, the agents in a Workflow’s nodes, a
workflow called as a tool, the debaters of a debate: all of it is one
tree under the run that started it.
Agents and versions
Studio catalogues every agent and workflow from its definition: the instructions, model, tools (and which are destructive), architecture, turn limit, permissions, guardrails, memory, output schema and effort, and its structure: the sub-agents in its team and how they’re connected, or a workflow’s steps, routes and branches. When any of these changes, the next run is recorded as a new version, with a note of what changed (“Model claude-haiku-4-5 to claude-haiku-5-5. Tool added: refund_charge.”, “Team or routing changed.”, “A team member’s tools changed.”). Nothing has to be tagged by hand.
Each version’s structure is what Studio draws as its
architecture diagram.
It’s taken with build_graph, from the
running objects, when the run starts.
An agent is keyed by its name=:
from loomflow import Agent
billing = Agent("You are the billing agent.", model="claude-haiku-5-5", name="billing-agent")Give your agents names. An unnamed agent is keyed by the first line of
its instructions, so editing that line starts a new catalog entry
instead of a new version. The name also labels the agent’s run span
(gen_ai.agent.name) and names its node in Workflow.chain.
Privacy
Studio writes only to the local file. Three things keep sensitive data out of it:
redact=scrubs every piece of text (prompts, replies, messages, tool arguments and results) before it’s written. Pass"pii"for loomflow’s PII redaction (emails, card numbers, SSNs, phone numbers), or anystr -> strfunction.capture_content=Falsekeeps no text at all: the steps, timing, tokens, cost, statuses and decisions stay, the content doesn’t.- Guardrails and Studio are separate. An agent’s
PIIGuardchanges what the user receives; Studio records what the model actually produced, which is what you need when debugging. If the file must not hold PII, useredact="pii"too.
Model messages and replies reach Studio on a private channel, never as
span attributes, so they never reach your OpenTelemetry exporter. Your
own telemetry keeps working unchanged alongside Studio, and gains two
plain attributes on tool spans: loom.permission and loom.approval.
Sampling
sample_rate=0.1 keeps a tenth of the traces. A trace is kept or
dropped whole, with its sub-runs.
Search
The search box finds runs by the words in them: the input and output,
user, session and run ids, and every step’s content (tool arguments
and results, model replies, errors). Every word you type has to
appear somewhere in the run, and matches the start of a word, so
refund finds “refunded” and 4471 finds “#4471”, but fund doesn’t
find “refund”. The messages a model call was sent aren’t searched:
each call repeats the conversation so far, which the other steps
already hold.
Studio keeps a full-text index for this (SQLite’s FTS5, in almost every Python build), so a search over tens of thousands of runs takes milliseconds. It builds the index for a file recorded before it existed the first time it records to it. If your SQLite has no FTS5, search falls back to scanning everything recorded, matching any part of the text.
Deleting old runs
enable(keep_days=30) (or LOOMFLOW_STUDIO_KEEP_DAYS=30) deletes runs
started more than 30 days ago, with their steps and scores, when
recording starts and then every hour. To clean a file once:
import loomflow.studio
deleted = loomflow.studio.prune(30) # or prune(30, path="...")Eval runs (experiments) and datasets are kept; delete the file to start over. A file Studio creates hands the space back as it deletes; a file created by loomflow 0.18.0 keeps its size and reuses the space.
Storage and timing
- Runs are written to
.loomflow/studio.dbby default (pass a path toenable, or setLOOMFLOW_STUDIOto one). Add.loomflow/to your.gitignore. - A trace’s row appears as
runningwhen it starts and is completed when it ends. Writing happens on a background thread, so a slow disk never slows a run. loomflow.studio.flush()waits until everything queued is written;await agent.aclose()does too, and the process flushes on exit.- If the disk can’t keep up and 10,000 writes are queued, new ones are dropped with a warning rather than blocking your app.
- Each piece of text is kept up to 32,000 characters, and a trace up to 5,000 steps (the rest are counted).
- Several processes can record to one file at once.
With Studio off, its hooks cost a few microseconds per run.