Skip to Content
StudioWhat gets recorded

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.

SwitchUse 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 recorded

loomflow.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:

StepWhat Studio keeps
Agent runAgent name and version, user, session, the prompt and the final output (and parsed output), turns, tokens, cost, how the run ended.
TurnTiming, and the model and tool calls inside it.
Model callThe 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 callArguments, 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 stepEach node’s output, and the agents that ran inside it.
EventGuardrail 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 any str -> str function.
  • capture_content=False keeps 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 PIIGuard changes 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, use redact="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.

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.db by default (pass a path to enable, or set LOOMFLOW_STUDIO to one). Add .loomflow/ to your .gitignore.
  • A trace’s row appears as running when 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.

Last updated on