Managed Figures
How Lobster AI 1.2.0 saves figures as immutable revisions with refs, how to list, edit and view them, and how to use versioned figure templates.
Overview
Since Lobster AI 1.2.0, figures in a local workspace are saved deliberately as immutable revisions. Every saved revision has a ref of the form <figure_id>@<revision_id>, for example f_k3m9q2xa7d@r_q7w2e4t6y3. An edit is saved as a new revision of the same figure; the original revision stays unchanged.
- A figure is saved only by an explicit save step: a visualization tool that commits its figure, or
submit_figure(...)inexecute_custom_code. - Nothing is captured automatically. A Plotly figure that is created but not submitted is not saved.
- Figure and revision IDs are opaque. Do not parse them for meaning or order.
Where Figures Come From
| Producer | How it saves |
|---|---|
visualization_expert plotting tools (create_umap_plot, create_qc_plots, create_violin_plot, create_feature_plot, create_dot_plot, create_heatmap, create_elbow_plot, create_cluster_composition_plot) | Save one revision when the tool succeeds. save_plot=False is a preview: nothing is saved |
execute_custom_code | Saves the figures the code passed to submit_figure(...), only if the code finished successfully |
Analysis and drawing are separate: the eight visualization tools listed above are explicit drawing producers, while figure templates draw from existing data without rerunning statistical analyses.
Saving Figures from Custom Code
Inside execute_custom_code, call submit_figure for every Plotly figure you want to keep:
import pandas as pd
import plotly.express as px
df = pd.DataFrame({
"gene": ["CD3E", "MS4A1", "LYZ"],
"log2fc": [2.1, -1.4, 0.3],
"neg_log10_p": [8.2, 5.1, 0.4],
})
fig = px.scatter(df, x="log2fc", y="neg_log10_p", hover_name="gene", title="Effect vs significance")
submit_figure(
fig,
source=df, # data the figure was drawn from (optional)
intent="log2 fold change against -log10 p", # what the figure should show (optional)
label="Effect vs significance", # defaults to the figure title
)submit_figure(fig, *, source=None, intent=None, parent=None, label=None, config=None):
| Argument | Meaning |
|---|---|
fig | A plotly.graph_objects.Figure. It is serialized at the call; later changes are not included |
source | The plotted data: a DataFrame, a list of records, or {"data": ..., "declared": {...}}. Without a source, nothing implies the figure can be regenerated |
intent | A short description (string) or a list of intent items |
parent | "<figure_id>@<revision_id>" when this is an edit of an existing figure |
label | A short human label; defaults to the figure title |
config | A Plotly config dict (JSON values only). Omitted: an edit keeps its parent's config, a new figure gets {}. An explicit {} clears it |
publish_plot(fig, title) is kept as an alias of submit_figure(fig, label=title).
Size limits: 64 MiB per figure, 64 MiB per source, 64 KiB per config.
Outcomes
The tool reply states what was saved:
| Situation | Reply text |
|---|---|
| Figure saved | Created <ref> ("<label>") → plots/<figure_id>/<revision_id>/figure.json |
| Figure not saved | Figure #<n> ("<label>") <status>: <reason> |
| Nothing saved | No figure saved. |
| Save not yet confirmed | Save confirmation pending. (not a failure) |
| Code failed | No figure saved: the code failed, so <n> submitted figure(s) were discarded. |
| Figure created but not submitted | Not saved: <variable names>. Call submit_figure(fig, source=…, intent=…) to save a figure. |
Saved refs are also returned in the reply's figure_refs field.
Editing a Saved Figure
A request about an existing figure, such as "make the title bigger" or "show only the top 20 genes", is an edit. Load the revision, change it, and submit it with the same ref as parent:
fig = load_figure("f_k3m9q2xa7d@r_q7w2e4t6y3")
fig.update_layout(title_font_size=24)
submit_figure(fig, parent="f_k3m9q2xa7d@r_q7w2e4t6y3")This saves a new revision of the same figure. The parent revision is kept, and so is its config unless you pass config=....
Reading Saved Figures in Custom Code
| Helper | Returns |
|---|---|
list_figures() | Saved revisions (latest and earlier) as of the start of this run |
load_figure(ref) | A saved revision as a Plotly figure |
load_figure_config(ref) | The saved config of a revision |
load_source(source_id) | A saved figure's source data as a DataFrame |
These helpers read a snapshot taken when the run starts. Within size limits, the latest revision of each figure is loaded, plus any ref written literally in the code. To load an earlier revision, write its ref literally. If a figure is unavailable, FigureUnavailable is raised with a reason, for example not_in_snapshot ("it did not exist when this attempt started"), not_requested ("write the ref literally in your code"), too_large, or figure_deleted.
Listing Figures
The workspace overview that agents read includes a Saved figures section, read from the saved records (it survives a restart). Each line shows:
<label> · <ref> · <kind> · source yes|no · edit of <parent ref>The 10 most recent revisions are listed. If the figure records cannot be read, the section says Not listed: <reason> instead of appearing empty.
On Disk
.lobster_workspace/plots/
|-- f_k3m9q2xa7d/ # one folder per figure
| |-- r_q7w2e4t6y3/ # one folder per revision
| | |-- revision.json
| | |-- figure.json
| | |-- config.json
| | |-- renditions/ # HTML views, when written
|-- plot_1_UMAP_cell_type.html # legacy flat filePaths inside a subfolder of plots/ belong to the managed figure namespace. The built-in write_file tool refuses writes there with the following message:
Error: '<path>' is inside the managed figure folder (plots/<figure>/...). Save figures with submit_figure(...) in execute_custom_code instead.Files directly inside plots/ from earlier versions are shown as legacy plot files: read-only, with no saved history.
The local store is designed for one Lobster AI process per workspace directory.
Viewing and Exporting
/plotwithout an argument writes any missing HTML views, then opens theplots/folder./plot <ref | figure id | label>opens the HTML view of that revision. If the HTML view cannot be written, the figure stays saved and the CLI saysFigure <ref> is saved; its HTML view failed (<reason>).- The local HTML-generation step uses the saved figure and config bytes and embeds plotly.js from the installed package, without fetching a CDN bundle. A view is not a new revision.
- A session export attempts to copy each saved revision's exact files, write or reuse its HTML view, and copy already recorded PNG renditions. It does not render new PNGs locally. The index records
png: not_rendered_locallywhen PNG generation was requested, ornullwhen it is disabled; recorded PNGs are still copied when possible. The CLI reports copy and view failures as⚠ Not copied: ..., and the package index and README retain the affected refs and reasons.
Figure Templates
Figure templates are ordinary, versioned Python functions that return editable Plotly figures. They never save anything: pass the result to submit_figure. When the figure-templates package is available and initializes successfully, execute_custom_code exposes list_templates, get_template, show_template_source and apply_house_style directly; otherwise do not assume those helpers are present:
list_templates() # names, versions, purposes, signatures
show_template_source("generic.scatter") # readable provider source
fig = get_template("generic.scatter@1")(
df, x="log2fc", y="neg_log10_p", id_col="gene", title="Effect vs significance"
)
fig.update_layout(title="Treated vs control") # every Plotly property stays editable
submit_figure(fig, source=df, intent="log2 fold change against -log10 p")get_template("name@version") pins a version. The following version 1 templates are supplied by these packages; only providers from installed packages appear in list_templates(). A missing package or template, or an unavailable requested version, raises KeyError when looked up. Provider import or manifest errors are reported rather than treated as absent templates.
| Package | Templates |
|---|---|
| Core | generic.scatter, generic.line, generic.bar, generic.histogram, generic.box, generic.heatmap, generic.correlation |
| lobster-proteomics | proteomics.volcano, proteomics.pathway_bubble |
| lobster-drug-discovery | chem.similarity_heatmap (from a precomputed matrix) |
| lobster-transcriptomics | sc.umap, sc.violin, sc.feature, sc.dot, sc.heatmap, sc.elbow, sc.cluster_composition, sc.qc |
Templates draw already computed results. They do not modify datasets, fit models or rerun statistical tests. They may compute descriptive summaries, such as group means, counts or correlations, without modifying the input dataset.
House style. Every shipped template embeds the omics_os house style. When template initialization succeeds in execute_custom_code, omics_os becomes Plotly's default for hand-written figures unless the code overrides it. apply_house_style(fig) embeds the same style in an existing figure; subsequent Plotly edits remain allowed.
Recipe metadata. A template records its name, version and resolved settings in layout.meta.lobster_template. This is declared metadata, not proof that a figure was produced by that template. Settings must be finite JSON values within 16 KiB; larger settings are refused, not truncated. Pass drawing data to the template's data argument (or other declared data_args); those arguments are excluded from recipe settings. Supply source=... separately to submit_figure to retain the plotted data, rather than embedding data in the recorded settings.
Adding Templates from a Package
A package can ship a uniquely named module in the lobster.figure_template_providers namespace (PEP 420: no __init__.py). The module exposes a TEMPLATE_MANIFEST of descriptors with name, version, purpose and callable, and decorates each drawing function with figure_template:
from lobster.figure_templates import figure_template
TEMPLATE_MANIFEST = [
{"name": "mypkg.ranked_bar", "version": "1", "purpose": "Ranked bar chart.", "callable": "ranked_bar"},
]
@figure_template("mypkg.ranked_bar", version="1")
def ranked_bar(data, *, x, y, title="Ranked"):
import pandas as pd
import plotly.express as px
table = pd.DataFrame(data).sort_values(y, ascending=False)
return px.bar(table, x=x, y=y, title=title)Keep heavy imports inside the drawing function so that discovery stays cheap. The default data argument is data; declare other input arrays with data_args=(...) so they are not recorded as settings. A duplicate template name, or a manifest version that does not match the decorator, raises ValueError at discovery.
Download Queue System
The Download Queue System is a structured handoff contract between researchagent and dataexpert, enabling coordinated dataset downloads with pre-valida...
LLM Price Snapshot (lobster.pricing)
Offline LLM list-price lookup from a reviewed models.dev snapshot, plus normalized token-usage evidence on the token callback. New in Lobster AI 1.2.0.