The Studio web app
Running it
pip install 'loomflow[studio]'
loom studio # reads .loomflow/studio.db
loom studio --db path/to/studio.db --port 7600 --open| Option | Default | |
|---|---|---|
--db | .loomflow/studio.db | The file to read. |
--host | 127.0.0.1 | The address to listen on. |
--port | 7600 | |
--open | off | Open the page in your browser. |
--agent MODULE:ATTR | An agent the page may run evals on, imported like loom eval --agent. Repeatable. | |
--read-only | off | Hide the forms: no scores, dataset cases or evals from the page. |
--allow-host NAME | Also answer to this host name, for example behind a proxy. Repeatable. |
The app reads the file while your app writes it, so you can leave it open and reload to see new runs. If the file doesn’t exist yet, the page tells you how to start recording.
Pages and URLs
Every view is a plain URL, so you can bookmark or share a filtered list or a single step.
| Page | URL | Filters |
|---|---|---|
| Traces | /traces | q (search), agent, version, user, model, status, problems=1, unscored=1, session, range (24h, 7d), project, page |
| A thread | /traces?session=<session_id> | Every run in one conversation. |
| A trace | /traces/<id> | span=<step id> selects a step. |
| A trace as JSON | /traces/<id>.json | Everything recorded for it. |
| Agents | /agents, /agents/<key> | project, version (which version’s diagram and definition to show) |
| Experiments | /experiments, /experiments/<id> | project |
| A comparison | /experiments/compare?a=<id>&b=<id> | show (all, changed, regressed) |
| Datasets | /datasets, /datasets/<id> | project |
| A dataset as JSONL | /datasets/<id>.jsonl | The file Dataset.from_jsonl reads. |
| Run an eval | /evals/new | dataset, agent (to pick them in the form) |
| Health | /health |
Scoring runs and building datasets
A trace’s page has a Scores bar under the run’s summary. It shows the run’s scores as chips (× removes one, wherever it came from) and has:
- Good and Bad: score the run in one click, under the key
review. - Other score…: any key (keys already in use are suggested), with Good, Bad, a number or a label, and an optional comment.
- Add to dataset…: the run’s input and output side by side, ready to edit. Change the output into the answer the agent should have given, pick a dataset (the last one you used is filled in; a new name creates one) and add the case.
Scores you add on the page are marked ui.
The Datasets page lists the datasets with their cases, and a
JSONL download; loomflow.studio.dataset("name") loads one for the
eval harness (Scores and experiments).
To work through runs, tick Not scored on the Traces page: it lists the runs nobody has scored yet.
Running evals from the page
Start Studio with the agents it may run, and the page can run an eval on a dataset and record it as an experiment:
loom studio --agent app.agents:billing --agent app.agents:supportor list them once in pyproject.toml, and run plain loom studio:
[tool.loomflow.studio]
agents = ["app.agents:billing", "app.agents:support"]Then use Run eval on a dataset’s page, Evaluate on a dataset on an agent’s page, or Run an eval on the Experiments page. The form picks:
- the agent (each shows what one of its runs has cost on average) and the dataset;
- how to score each case: the output contains the expected answer
(ignoring case), matches it exactly, or an LLM judge’s score, with
the judge model you name (for example
gpt-5-mini); - a name, a spending limit (it stops starting cases once the runs have cost that much; empty for none) and how many cases run at once.
The experiment’s page opens at once and refreshes itself while the eval runs, with a Cancel button. When it ends it links to Compare with the previous run of the same dataset. One eval runs at a time.
Studio imports your agents into its own process, the way loom eval
does, and runs them there: with their tools, guardrails and model, so
the process needs the models’ API keys in its environment (or loaded
by your module, as most apps do). Runs cost what your agent’s runs
cost. Only the agents you list can run, and --read-only turns
evals off. Agents only, for now: a Workflow can’t be evaluated yet.
Architecture diagrams
An agent’s page opens with a diagram of the version you’re looking at, with a tab for each version so you can see how the structure changed. A trace’s page shows the diagram of the version that ran, with the parts that run used highlighted and the rest dimmed.
What the diagram shows:
- Agents by their
name=(or the first line of their instructions), with the team’s architecture under the name (supervisor,router,swarm, …) and the tools each one holds. - Teams: a supervisor’s
delegateandforwardedges to its workers, a router’s routes, an actor and critic loop, debaters and their judge, a blackboard. Edges that loop back up are dashed; a swarm whose every member can hand off to every other gets a note on its group instead of a tangle of lines. - Workflows:
start, each step (a function, an agent or a nested workflow), routes labelled with their keys, parallel branches, andend.
Diagrams are drawn as SVG on the server, like the rest of the app, with no JavaScript. Under each one, As text lists the same connections for screen readers or copying.
Mounting it in another app
create_app returns a plain ASGI app, like
loomflow.serve, so it runs under any ASGI
server or mounts inside a Starlette or FastAPI app:
from loomflow.studio.web import create_app
studio_app = create_app(
".loomflow/studio.db",
read_only=False, # True hides the forms
allowed_hosts=["studio.internal"], # the name you reach it by
agents=["app.agents:billing"], # or Agent objects: evals from the page
)The app answers only to the host names in allowed_hosts (by default
localhost, 127.0.0.1, ::1 and any *.localhost), so when you
serve it under another name, list that name; ["*"] answers to any.
Keeping it private
The app has no login. Traces hold prompts, replies and tool payloads,
so keep it on 127.0.0.1 (the default) or put it behind your own
authentication. Don’t expose it on a public address.
The app itself is careful with what it serves and accepts:
- It changes only what you add from its pages, scores and dataset
cases, and never your recorded runs.
--read-onlyturns even that off. - A form is accepted only with a token from the app’s own pages, and refused when the browser says it came from another site, so a web page you visit can’t post to Studio.
- It answers only to its own host names, so a web page can’t read it
by pointing a name of its own at 127.0.0.1.
loom studio --hostadds the address it listens on; listening on0.0.0.0answers to any name, andloom studiowarns you that anyone who can reach the port can read your traces. - It serves no JavaScript, every recorded value is HTML-escaped, and its pages carry a Content-Security-Policy that forbids scripts and outside requests, so a prompt containing markup can’t run in your browser.