Omics-OSDocs
Advanced

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(...) in execute_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

ProducerHow 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_codeSaves 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):

ArgumentMeaning
figA plotly.graph_objects.Figure. It is serialized at the call; later changes are not included
sourceThe plotted data: a DataFrame, a list of records, or {"data": ..., "declared": {...}}. Without a source, nothing implies the figure can be regenerated
intentA short description (string) or a list of intent items
parent"<figure_id>@<revision_id>" when this is an edit of an existing figure
labelA short human label; defaults to the figure title
configA 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:

SituationReply text
Figure savedCreated <ref> ("<label>") → plots/<figure_id>/<revision_id>/figure.json
Figure not savedFigure #<n> ("<label>") <status>: <reason>
Nothing savedNo figure saved.
Save not yet confirmedSave confirmation pending. (not a failure)
Code failedNo figure saved: the code failed, so <n> submitted figure(s) were discarded.
Figure created but not submittedNot 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

HelperReturns
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 file

Paths 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

  • /plot without an argument writes any missing HTML views, then opens the plots/ 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 says Figure <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_locally when PNG generation was requested, or null when 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.

PackageTemplates
Coregeneric.scatter, generic.line, generic.bar, generic.histogram, generic.box, generic.heatmap, generic.correlation
lobster-proteomicsproteomics.volcano, proteomics.pathway_bubble
lobster-drug-discoverychem.similarity_heatmap (from a precomputed matrix)
lobster-transcriptomicssc.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.

On this page