Skip to content

Add a view provider ​

A view provider is a Python package that teaches Studio one format. Install one in the notebook's environment, and its starters appear in the New view dialog beside the built-in ones:

The New view dialog lists Acme report and Acme dashboard under acme-views, above the built-in starters from marimo-studio, and shows the files the selected Acme report creates

This page builds acme-views, the package in that dialog. Acme report starts a plain HTML page and needs only Python. Acme dashboard adds a Lit component bundled by Vite.

Advanced feature

The view provider API is an advanced extension point. Pin marimo-studio to the minor line you test, such as marimo-studio>=0.4,<0.5.

What a provider receives and returns ​

A provider works with the files of a view project. Studio calls its methods at fixed moments and validates each record they return:

WhenStudio callsThe provider receivesThe provider returns
The New view dialog opensavailability(), starters()NothingWhether it can run, and its starters
Someone creates a viewcreate()The chosen starter and the saved notebook's cellsThe first files of the view project
Source opens, or a file in the view project changesinspect()The view projectIts source documents, build inputs, hosts, and renders
Preview, marimo run, or an export needs a new pagebuild()A build snapshot of the build inputsA page: an HTML entry file and the files it loads
A notebook result that a render reads changesrender()The render template, values as JSON, and outputs and cells as media, such as images or PDFsRendered files, such as a PDF

A provider never runs the notebook. build() writes a page whose hosts are still empty, and Studio fills them from the session while the page is open, as How Studio works describes. render() is the one method that receives notebook results, and they arrive already converted to JSON and images.

Choose a pattern ​

Every provider imports one module, marimo_studio.view_providers. Pick the pattern closest to your tool:

PatternThe buildSDK piecesExample
HTML pagecopies or templates HTMLhtml_sites, copy_inputsthis guide's ReportProvider
Framework buildbundles JavaScript with a pinned toolchainrequest.runner, request.work_root, request.cache_rootdeno_react
Command-line toolruns an installed tool that writes HTMLprobe_tool, copy_inputs, ProviderErrorquarto
Rendered filerenders files, such as a PDF, from notebook resultsRender, render_page, render()typst

When no pattern fits, start from the HTML page provider and replace build() with your tool's steps. The built-in Quarto provider also imports private _builtin.quarto helpers, so copy its use of the public module and keep your provider's imports to marimo_studio.view_providers.

Ask a coding agent ​

Open a coding agent in the notebook's project and paste this prompt:

text
Read https://marimo-team.github.io/marimo-studio/guide/view-providers.md
and https://marimo-team.github.io/marimo-studio/reference/provider-api.md.

Create the guide's acme-views package in ./acme-views for analysis.py, with
the Acme report starter and the Acme dashboard starter, a Vite page with a
Lit header and every output cell. Follow "Add a framework build" and the
"Provider rules". Install the package with `uv add --editable ./acme-views`,
run every command in "Verify the provider", and fix each failure before
you stop.

For another framework, replace Lit with its name, and see Swap Lit for a JSX framework. The rest of the page builds the same package by hand.

Files in a view project ​

Pick Acme report and name the view briefing. Studio creates the view project in the notebook's views folder, studio/analysis/ by default:

text
studio/analysis/briefing/
  view.toml    names the provider that owns this view
  index.html   the page, with one <marimo-cell> host per output cell
  AGENTS.md    instructions for coding agents that edit this view

The provider writes every file except view.toml and decides how Studio treats it. A source document appears in Studio's Source pane for editing. A build input rebuilds the page when it changes.

FileWritten bySource documentBuild input
index.htmlthe provider, at creationyesyes
AGENTS.mdthe provider, at creationyesno
DESIGN.mdthe user or an agent, lateryes, once it existsno
view.tomlStudioCLI and Python onlyyes

AGENTS.md tells a coding agent which file holds the page, what the <marimo-cell> hosts mean, and where decisions belong. The marimo-studio agent skill reads it before editing a view, so ship one written for your framework. DESIGN.md records lasting decisions such as audience, visual direction, and interaction priorities once the view has a direction. Editing either file never rebuilds the page.

How Studio finds the provider ​

The package registers the provider in the marimo_studio.view_providerentry point group, the Python packaging mechanism that lets an installed package publish objects to other packages. Studio derives these names from the registration:

NameValueMade from
Registrationreport = "acme_views:provider"Entry point in the acme-views package
Provider keyacme-views/reportDistribution name and registration name
Starter IDacme-views/report:defaultProvider key and the starter key
view.tomlprovider = "acme-views/report"Written to the view project at creation

Studio reads the entry points once per process, from its own Python environment. Restart marimo edit after you install a provider or change its starters. An unavailable provider's starters appear disabled with the recovery action it returned.

Create the package ​

From the notebook's project, create a library with uv, the Python package manager Studio uses to resolve notebook environments:

console
uv init --lib acme-views
cd acme-views
uv add "marimo-studio>=0.4,<0.5"

When the notebook's directory is a uv project, uv init adds acme-views to its workspace. Register the provider in acme-views/pyproject.toml:

toml
[project.entry-points."marimo_studio.view_provider"]
report = "acme_views:provider"

Write the provider ​

Replace src/acme_views/__init__.py with:

python
import html
from pathlib import PurePosixPath

from marimo_studio.view_providers import (
    BuildRequest,
    BuildResult,
    InspectionRequest,
    BuildInput,
    ProjectInspection,
    ProviderAvailability,
    ProviderInfo,
    ProviderStarter,
    SourceDocument,
    StarterContext,
    StarterPlan,
    copy_inputs,
    html_sites,
)

ENTRY = PurePosixPath("index.html")
AGENTS = PurePosixPath("AGENTS.md")

AGENTS_SOURCE = """# Acme view

Studio builds this view from `index.html` with the Acme provider.

- Each `<marimo-cell name="...">` in `index.html` shows the notebook cell with
  that name. Move or remove hosts freely and keep their `name` values.
- Keep calculations in the notebook, and layout and wording in this project.
- Record audience and visual decisions in `DESIGN.md`.
"""


class ReportProvider:
    info = ProviderInfo(
        title="Acme report",
        summary="One HTML page with the notebook's output cells.",
    )
    starter = ProviderStarter(
        key="default",
        title="Acme report",
        summary="Start from one HTML page with every output cell.",
        documents=(ENTRY, AGENTS),
    )

    def availability(self) -> ProviderAvailability:
        return ProviderAvailability(True)

    def starters(self) -> tuple[ProviderStarter, ...]:
        return (self.starter,)

    def create(self, starter: ProviderStarter, context: StarterContext) -> StarterPlan:
        cells = context.output_cells
        hosts = "\n".join(
            f'      <marimo-cell name="{html.escape(item.target)}"></marimo-cell>'
            for item in cells
        )
        page = f"""<!doctype html>
<html lang="en">
  <head><meta charset="utf-8"><title>{html.escape(context.title)}</title></head>
  <body>
    <main id="app-shell">
{hosts}
    </main>
  </body>
</html>
"""
        files = {ENTRY: page.encode(), AGENTS: AGENTS_SOURCE.encode()}
        return StarterPlan(files=files, cell_targets=cells)

    def inspect(self, request: InspectionRequest) -> ProjectInspection:
        source = (request.project.root / ENTRY).read_bytes()
        sites, diagnostics = html_sites(ENTRY, source)
        return ProjectInspection(
            documents=(SourceDocument(ENTRY, "html", "edit"),),
            inputs=(BuildInput(ENTRY, "file"),),
            sites=sites,
            diagnostics=diagnostics,
        )

    def build(self, request: BuildRequest) -> BuildResult:
        copy_inputs(request, request.staging_root)
        return BuildResult(ENTRY)


provider = ReportProvider()

Each method answers one moment from What a provider receives and returns.

availability() and starters(): fill the New view dialog ​

ReportProvider needs only Python, so availability() always returns ProviderAvailability(True). A provider that runs an installed tool returns probe_tool(("tool", "--version"), minimum=..., install=...), which reports a missing or outdated tool with your install instructions.

starters() returns one ProviderStarter. Its title names the starter in the list, and summary and documents fill the starter's details. Add a format, an SVG icon, a preview screenshot, a homepage, and settings to present the starter the way the built-in ones are, as Present a starter in the New view dialog describes. Studio keeps the starter list for the life of the process.

create(): write the starting files ​

create() receives the saved notebook in context.notebook and returns every file of the new project in a StarterPlan. Studio writes the files. When creating the view adds Studio configuration or cell aliases to the notebook, Studio calls create() again with the configured notebook, so return the same plan for the same notebook. ReportProvider writes one <marimo-cell> host into index.html for each cell in context.output_cells, and adds AGENTS.md.

context.output_cells lists the cells that may display output. It leaves out disabled cells and their descendants, because marimo never runs them.

context.cell_targets supplies the name each host uses. A named cell such as summary keeps its name. For an unnamed cell, Studio proposes an alias such as cell-2 and saves it in the notebook when the provider returns that target in StarterPlan.cell_targets.

inspect(): describe the project ​

inspect() returns a ProjectInspection with the project's documents, build inputs, and projection sites. Studio also calls it before each build.

documents lists the Source documents, here index.html. Studio adds view.toml, AGENTS.md, and DESIGN.md itself. inputs lists the build inputs, so Studio rebuilds when index.html or view.toml changes. For a project with many files, project_files(request.project) lists every file that can affect a build.

sites holds one projection site per <marimo-cell>, <marimo-output>, or mo-value host, the spot in the page that shows the result it names. html_sites() records each host's line and column for Source diagnostics, and reports a malformed host.

build(): publish the page ​

request.project.root holds a snapshot of the build inputs, in which Studio has already marked each site's host so it can connect the host to the notebook. The index.html in Source keeps its original markup. ReportProvider copies its inputs to request.staging_root with copy_inputs(). Studio validates the staged files and publishes them as a new revision. A failed build leaves the last good revision in Preview.

Install it and create a view ​

From the notebook's project, install the package and check that Studio finds it:

console
uv add --editable ./acme-views
uv run marimo-studio doctor acme-views/report
text
available acme-views/report
  distribution acme-views
  registration report
  installed 0.1.0
  One HTML page with the notebook's output cells.
  starters acme-views/report:default

Start uv run marimo edit analysis.py, open the view menu, and choose New view. Acme report appears under From acme-views, with Files created listing index.html, AGENTS.md. Or create the view from the CLI:

console
uv run marimo-studio view create briefing \
  --target analysis.py \
  --starter acme-views/report:default

Source shows one host per output cell in the page. Give the view a design direction by saving studio/analysis/briefing/DESIGN.md:

md
# Design

Audience: the weekly operations review.
Lead with the threshold control, then its result.

Studio lists DESIGN.md at its next inspection, such as after the next save of index.html:

Source pane for the briefing view with index.html, AGENTS.md, and DESIGN.md tabs, showing marimo-cell hosts named cell-2 and summary

Every environment that edits, builds, or exports the view needs the provider package. Publish it to a package index, or declare it as a path or Git source in the notebook's project.

Add a framework build ​

Acme dashboard bundles a Lit component with Vite, a frontend build tool that needs a JavaScript runtime. The machine that opens the notebook may lack Node.js, so the provider uses Deno, a JavaScript runtime that also installs npm packages. The deno package on PyPI installs it into the same environment as Studio, and deno.find_deno_bin() returns its path.

Add the dependency from acme-views and register a second provider:

console
uv add deno
toml
[project.entry-points."marimo_studio.view_provider"]
report = "acme_views:provider"
vite = "acme_views.vite:provider"

Pin the dashboard's npm packages ​

The dashboard starter copies three files from src/acme_views/dashboard/ into every new view.

main.js defines the Lit header:

js
import { html, LitElement } from "lit";

customElements.define(
  "acme-header",
  class extends LitElement {
    render() {
      return html`<h1>Acme dashboard</h1>`;
    }
  },
);

deno.json pins Lit and Vite:

json
{
  "nodeModulesDir": "auto",
  "imports": {
    "lit": "npm:lit@3.3.1",
    "vite": "npm:vite@7.1.7"
  }
}

Generate deno.lock beside them:

console
cd src/acme_views/dashboard
uv run -- deno install --lockfile-only

The lockfile records the version and checksum of every package in the dependency tree. Rerun the command after you change deno.json.

Write the Vite provider ​

Create src/acme_views/vite.py:

python
import os
from dataclasses import replace
from importlib.resources import files
from pathlib import Path, PurePosixPath

from deno import find_deno_bin
from marimo_studio.view_providers import (
    BuildRequest,
    BuildResult,
    InspectionRequest,
    BuildInput,
    ProjectInspection,
    ProviderAvailability,
    ProviderCommandResult,
    ProviderError,
    ProviderInfo,
    ProviderStarter,
    SourceDocument,
    StarterContext,
    StarterPlan,
    copy_inputs,
)

from acme_views import AGENTS, ENTRY, ReportProvider

MAIN = PurePosixPath("main.js")
CONFIG = PurePosixPath("deno.json")
LOCK = PurePosixPath("deno.lock")
TEMPLATE = files("acme_views") / "dashboard"


def installed(work: Path, pattern: str) -> str:
    """Return the resolved paths of installed npm files that match `pattern`."""
    paths = sorted((work / "node_modules" / ".deno").glob(pattern))
    if not paths:
        raise ProviderError(f"deno install provided no {pattern}.")
    return ",".join(str(path.resolve()) for path in paths)


def check(step: str, result: ProviderCommandResult) -> None:
    if result.returncode != 0:
        raise ProviderError(
            f"{step} failed with exit code {result.returncode}: "
            f"{result.stderr.strip()[-2000:]}",
            hint="Fix the error in the view's source, then build again.",
            code="vite-build-failed",
        )


class ViteProvider(ReportProvider):
    info = ProviderInfo(
        title="Acme Vite",
        summary="A Vite page with Lit components and the notebook's output cells.",
    )
    starter = ProviderStarter(
        key="default",
        title="Acme dashboard",
        summary="Start from a Vite page with a Lit header and every output cell.",
        documents=(ENTRY, AGENTS, MAIN, CONFIG, LOCK),
    )

    def availability(self) -> ProviderAvailability:
        try:
            find_deno_bin()
        except FileNotFoundError:
            return ProviderAvailability(
                False,
                reason="The deno package is not installed.",
                action="Install the deno package where Studio runs.",
            )
        return ProviderAvailability(True)

    def create(self, starter: ProviderStarter, context: StarterContext) -> StarterPlan:
        plan = super().create(starter, context)
        page = plan.files[ENTRY].decode()
        page = page.replace(
            '<main id="app-shell">\n',
            '<main id="app-shell">\n      <acme-header></acme-header>\n',
        ).replace(
            "  </body>",
            '    <script type="module" src="./main.js"></script>\n  </body>',
        )
        project_files = {**plan.files, ENTRY: page.encode()}
        for path in (MAIN, CONFIG, LOCK):
            project_files[path] = (TEMPLATE / path.name).read_bytes()
        return StarterPlan(files=project_files, cell_targets=plan.cell_targets)

    def inspect(self, request: InspectionRequest) -> ProjectInspection:
        inspection = super().inspect(request)
        return replace(
            inspection,
            documents=(
                *inspection.documents,
                SourceDocument(MAIN, "javascript", "edit"),
                SourceDocument(CONFIG, "json", "edit"),
                SourceDocument(LOCK, "json", "read"),
            ),
            inputs=(
                *inspection.inputs,
                BuildInput(MAIN, "file"),
                BuildInput(CONFIG, "file"),
                BuildInput(LOCK, "file"),
            ),
        )

    def build(self, request: BuildRequest) -> BuildResult:
        work = request.work_root.resolve()
        output = request.staging_root.resolve()
        copy_inputs(request, work)

        deno = find_deno_bin()
        cache = {"DENO_DIR": str(request.cache_root / "deno")}
        result = request.runner.run(
            [deno, "install", "--frozen"],
            cwd=work,
            environment={**os.environ, **cache},
        )
        check("deno install", result)

        # Stop Vite's package and workspace search at the working directory.
        (work / "package.json").write_text('{ "private": true }\n', encoding="utf-8")
        (work / "pnpm-workspace.yaml").write_text("packages: []\n", encoding="utf-8")
        vite = [
            deno,
            "run",
            "--cached-only",
            "--frozen",
            "--no-prompt",
            f"--allow-read={work},{output}",
            f"--allow-write={output}",
            "--allow-env",
            "--allow-sys=uid,osRelease",
            f"--allow-ffi={installed(work, '@rollup+rollup-*/**/*.node')}",
            f"--allow-run={installed(work, '@esbuild+*/**/esbuild*')}",
            "node_modules/vite/bin/vite.js",
            "build",
            "--configLoader=native",
            "--base",
            "./",
            "--outDir",
            str(output),
        ]
        check("vite build", request.runner.run(vite, cwd=work, environment=cache))
        return BuildResult(ENTRY)


provider = ViteProvider()

ViteProvider reuses the report provider and changes four methods:

  • availability() checks for the deno binary, so the New view dialog shows the starter's recovery action and keeps Create view disabled when it is missing.
  • create() copies main.js, deno.json, and deno.lock into the new view, and adds an <acme-header> element and a <script> tag to index.html.
  • inspect() adds the three files to Source and to the build inputs. deno.lock opens read-only.
  • build() copies the inputs to request.work_root, a private directory that Studio empties after the build. deno install --frozen installs the locked packages into node_modules and fails when deno.json and deno.lock disagree. Vite then bundles main.js into request.staging_root and keeps the site attributes on the hosts in index.html.

DENO_DIR keeps Deno's downloads in request.cache_root, which persists between builds, so only the first build downloads Vite and Lit. request.runner.run() stops a command when Studio cancels the build, and all commands in one build share a 120-second budget. check() turns a nonzero exit into a ProviderError that marimo-studio view build prints with the end of the command's error output.

Sandbox the build ​

npm packages run code inside Vite, so build() grants Vite only the Deno permissions a build needs. Deno checks the file, network, and environment access of JavaScript code against these grants:

FlagAllows
--allow-read with the work and outputreading the working copy and the built files
--allow-write with the outputwriting the built page
--allow-env, --allow-sys=uid,osReleasethe environment and system details Vite reads
--allow-ffi with Rollup's .node bindingloading Rollup's native bundler
--allow-run with the esbuild binarystarting esbuild, which transforms the code

Rollup's native binding and the esbuild process run outside those checks, with the operating-system authority of the Studio process, as Deno's permissions that bypass the sandbox describes. deno.lock pins both, so review a lockfile change before a build installs it.

The Vite command receives DENO_DIR as its whole environment, so tokens and other variables from the Studio process stay out of the build. --cached-only and --frozen limit it to the packages deno install placed. The package.json and pnpm-workspace.yaml that build() writes stop Vite's project root search from reading parent directories, and --configLoader=native loads a vite.config.js without writing a temporary file.

Swap Lit for a JSX framework ​

To build the dashboard header with Preact, change the starter in four places:

  • Name the entry module main.jsx, point the <script> tag at it, and list it with the javascriptreact language.

  • Add vite.config.js to src/acme_views/dashboard/, the files create() copies, ProviderStarter.documents, and the inspection's documents and inputs:

    js
    export default { esbuild: { jsx: "automatic", jsxImportSource: "preact" } };
  • Replace Lit with "preact": "npm:preact@10.27.2" in deno.json and regenerate deno.lock. Vite resolves subpaths such as preact/hooks from node_modules.

  • Render components into their own element, such as <div id="acme-header">, and keep the <marimo-cell> hosts outside the component tree so the framework never replaces them.

To place hosts inside components, extend inspect() to report them as ProjectionSite records from the component source, as the built-in providers in view_providers/_builtin do for JSX and Svelte.

Publish a rendered file ​

A provider can declare a render: files it makes from one project file and notebook results with its own tools. Its page shows the render with <marimo-output render="card.svg">, named by that project file, and Studio renders it again whenever a notebook result it reads changes. The host shows the main file, such as a PDF, an image, or a video, with a download link.

Register card = "acme_views.card:provider", then create src/acme_views/card.py:

python
from pathlib import PurePosixPath
from xml.sax.saxutils import escape

from marimo_studio.view_providers import (
    BuildInput,
    BuildRequest,
    BuildResult,
    InspectionRequest,
    ProjectInspection,
    ProviderAvailability,
    ProviderInfo,
    ProviderStarter,
    Rect,
    Render,
    RenderFile,
    RenderRegion,
    RenderRequest,
    RenderResult,
    RenderValue,
    SourceDocument,
    SourceLocation,
    StarterContext,
    StarterPlan,
    copy_inputs,
    render_page,
)

CARD = PurePosixPath("card.svg")
TEMPLATE = """<svg xmlns="http://www.w3.org/2000/svg" width="240" height="80">
  <text x="16" y="48">Total: {{ total }}</text>
</svg>
"""


class CardProvider:
    info = ProviderInfo(
        title="Acme card",
        summary="An SVG card with the notebook's report total.",
    )
    starter = ProviderStarter(
        key="default",
        title="Acme card",
        summary="Start from an SVG card that shows report.total.",
        documents=(CARD,),
    )

    def availability(self) -> ProviderAvailability:
        return ProviderAvailability(True)

    def starters(self) -> tuple[ProviderStarter, ...]:
        return (self.starter,)

    def create(self, starter: ProviderStarter, context: StarterContext) -> StarterPlan:
        return StarterPlan(files={CARD: TEMPLATE.encode()}, cell_targets=())

    def inspect(self, request: InspectionRequest) -> ProjectInspection:
        return ProjectInspection(
            documents=(SourceDocument(CARD, "xml", "edit"),),
            inputs=(BuildInput(CARD, "file"),),
            renders=(
                Render(
                    CARD,
                    values=(RenderValue("report.total", SourceLocation(CARD, 2, 30)),),
                ),
            ),
        )

    def build(self, request: BuildRequest) -> BuildResult:
        copy_inputs(request, request.template_root)
        return BuildResult(render_page(request, CARD))

    def render(self, request: RenderRequest) -> RenderResult:
        template = (request.template_root / request.source).read_text("utf-8")
        total = request.values.get("report.total")
        text = "pending" if total is None else str(total)
        card = template.replace("{{ total }}", escape(text))
        (request.output_root / CARD).write_text(card, encoding="utf-8")
        # The total sits across the middle of the card, so Lens can select it.
        total_region = RenderRegion(
            CARD,
            Rect(x=0.05, y=0.35, width=0.7, height=0.35),
            reads=("report.total",),
            label="Report total",
        )
        return RenderResult(
            (RenderFile(CARD, "image/svg+xml"),), main=CARD, regions=(total_region,)
        )


provider = CardProvider()

Render(CARD, values=...) declares that the provider renders card.svg and names the notebook values it reads, at the place it reads them. Each value arrives in its JSON form, so a dataframe arrives as a list of row objects and a date as ISO 8601 text.

build() copies the declared build inputs into request.template_root, which Studio keeps private, and writes a page with one <marimo-output render="card.svg"> host with render_page(). A provider that builds its own page places the host beside its other content and hosts.

Studio calls render() with a writable copy of the template in request.template_root, the file to render in request.source, an empty request.output_root, and the reader's current results. A result without a current value is absent and a null result is None, so the template shows its default for both. RenderResult lists the files to publish with their media types and names the main file. A render that places figures or named cell outputs reads them with RenderOutput or RenderCell. Render files from notebook results defines the contract, including figure sizes and preview files.

Make parts of the file selectable ​

A rendered file is a picture of results, so Studio cannot tell on its own which pixels show which result. render() returns a render region for each part the provider placed, and Lens can select that part and trace it to its cell. CardProvider returns one region, total_region, for the total across the middle of the card.

Rect measures fractions of the file's width and height from its top left corner. A PDF region also names its page, and a video region the start and end seconds of the frames that show the part. Studio drops a region that leaves its file or names no result the render reads, and warns once. Locate parts of rendered files defines every field.

Provider rules ​

Studio checks each result against these rules and reports the one it breaks.

  • Implement every method as a synchronous function. Raise ProviderError for a problem the author can fix, and pass source= when it points at a file.
  • Declare every provider option in info.options. Studio rejects a view whose view.toml sets any other option.
  • List every file the build reads in inputs. The build snapshot holds only those files from the view project.
  • Keep inspect() read-only. Put reusable data in request.cache_root.
  • Report each host as a ProjectionSite whose offset points inside its start tag in the stored file. html_sites() does this for HTML. Match kind to the host: cell for marimo-cell, output for marimo-output, value for an element with mo-value.
  • Build from the snapshot in request.project.root and keep the data-marimo-studio-site attribute on each host in the built files.
  • Write the page inside request.staging_root and render templates inside request.template_root. The page needs one head, one body, and one element with id="app-shell".
  • Declare each render by its source file, a build input, and show only those files in <marimo-output render="..."> hosts.
  • Run build() commands with cwd inside request.project.root, which holds request.work_root, request.staging_root, and request.template_root. Run render() commands inside request.template_root.
  • Pass environment={**os.environ, ...} to add variables. A mapping replaces the command's whole environment.
  • Pin npm packages in a lockfile that ships with the starter, and install them with deno install --frozen.
  • Run build tools that execute npm code with scoped Deno permissions, as in Sandbox the build.
  • Use a kebab-case diagnostic code and a non-empty message without leading or trailing whitespace.

Studio rebuilds a view when its inputs, the provider package version, or the tool version from availability() change. Release a new version when the build output changes for the same inputs.

Verify the provider ​

Add check_provider() to the provider's tests. It runs each starter through Studio's steps and raises ProviderCheckError with the message Studio would show:

python
from marimo_studio.view_providers.testing import check_provider

from acme_views import provider


def test_report_provider() -> None:
    (view,) = check_provider(provider)
    assert "index.html" in {path.as_posix() for path in view.published}

Pass the provider object while you develop it, or the installed key, such as check_provider("acme-views/report"), to load it the way Studio does. Pass options={"entrypoint": "page.html"} to check a provider option. check_provider() lists every check.

Then check the installed provider end to end from the notebook's project:

console
uv run marimo-studio doctor acme-views/vite
uv run marimo-studio view create dash --target analysis.py \
  --starter acme-views/vite:default
uv run marimo-studio view build dash --target analysis.py
uv run marimo run analysis.py
CommandExpected result
doctor acme-views/viteFirst line available acme-views/vite, exit status 0
view createCreated view dash
view buildBuilt dash for development use
marimo runhttp://localhost:2718/dash/ shows the Lit header and the notebook's cells

marimo run builds the production profile on the first request. The provider works when /dash/ shows the notebook's controls and outputs and no This section is unavailable. placeholder.

Troubleshoot ​

Message or symptomFix
Unknown view provider 'acme-views/vite'Install the package in the environment that runs Studio, check the entry point, and restart marimo edit.
view.toml sets 'port', which Acme Vite does not read.Add the option to info.options, or remove it from view.toml.
Provider working directory is outside the view snapshotRun commands in request.work_root or request.project.root.
expected one element with id="app-shell"Keep one #app-shell element in the entry document.
projection-site-missing, and the page shows This section is unavailable. in place of a cellBuild from request.project.root and keep each host's data-marimo-studio-site attribute.

The View provider API defines every record and term on this page.