Scrydon
Analytics

Marimo notebooks

Python notebooks scoped to a workspace and your managed tables — reactive, governed, no separate data-science stack required.

Marimo notebooks are Python notebooks that run inside your Scrydon cluster, scoped to a workspace, and pre-wired to your managed tables. They're reactive — cells re-run when their inputs change — and they go through the same governance as everything else.

Open first, start compute when you need it

Opening a notebook is deliberately separate from running Python. Scrydon authenticates the page, reads the notebook document, and renders native editable Marimo cells without starting a kernel or allocating an isolated workload. You can inspect and edit cells immediately while the status remains Not connected.

Compute starts in either of two ways:

  • Connect prepares an isolated runtime, reconciles dependencies, and connects the Marimo kernel without running any cell.
  • Run on a cell while disconnected performs that same preparation, then sends the selected Run exactly once after the kernel is ready.

Both paths show the current phase: authorizing, provisioning isolated compute, loading source, syncing dependencies, connecting governed storage, and ready. Repeated clicks, browser retries, or another tab joining the same preparation do not create duplicate runtimes.

Edits made before connecting are included when the runtime starts. Merely opening or editing the document does not install packages, mint a storage delegation, or call the Runtime Plane.

What they're for

Use caseWhy marimo
Ad-hoc analysis on a managed tableNo need to export — query the table from Python with the user's masks already applied
Build a one-off reportReactive cells make iterating quick
Sketch out a feature engineering pipeline before turning it into a workflowSame data surface, same governance
Run model evaluations against historical dataNotebooks can call back into the workflow engine for inference

How they're scoped

A notebook is owned by a workspace and authenticated against the user who opened it:

  • Opening the document grants no compute or storage credential.
  • When compute is requested, Scrydon creates a short-lived, notebook- and runtime-bound delegation inside a trusted broker. The Python kernel receives no bearer token, database password, cloud credential, Dapr identity, or Kubernetes token.
  • Reads apply your column masks — the same policies the Analytics table preview enforces.
  • Reads and writes re-check your live workspace membership and access policy on every brokered operation and land in the audit trail under your identity.
  • The runtime cannot reach the raw Iceberg catalog or storage provider credentials.

There is no marimo-admin super-user that bypasses governance — the notebook is just another caller.

The scrydon data SDK

Every notebook has the scrydon Python library pre-installed. It gives you a governed read/write loop against your workspace's managed tables:

import scrydon
from scrydon import tables

scrydon.whoami()            # who am I connected as, and until when?

tables.list()               # DataFrame of the workspace's managed tables
tables.get("suppliers")     # schema + governance flags (masked/hidden per column)

# Read — column masking already applied, paginated under the hood
df = tables.read("suppliers", limit=10_000)
df.attrs["scrydon"]         # {"table_id": ..., "masked_columns": [...], ...}

# Write back — append | upsert | replace
tables.write("suppliers", df, mode="append")

# Publish a brand-new managed table from a DataFrame
tables.create("supplier_risk_scores", scores_df, classification="internal")

# Idempotent create for re-run notebooks: reuse the table if it already exists
# instead of raising (its schema and rows are left as-is — add rows with write()).
tables.create("supplier_risk_scores", scores_df, classification="internal", exist_ok=True)

Everything a create or write produces is a first-class managed table backed by the Iceberg lakehouse — the same storage every Analytics-created table uses. It shows up in Analytics → Tables immediately (never as a "Legacy" StarRocks-native table), takes column access policies, can be bound to ontology object types, and is readable by workflows, chat tools, and BYO Iceberg engines.

Mask round-trip guard. If your role sees masked placeholder values in a column, tables.write refuses to write those columns back — writing placeholders would silently corrupt the table. Drop the masked columns from your DataFrame first (they're listed in df.attrs["scrydon"]["masked_columns"]).

Workspace objects are also available through bounded, governed storage helpers and the standard fsspec interface:

from scrydon import fsspec, storage

with fsspec.open("workspace://reports/summary.json", "rb") as source:
    summary = source.read()

content, observed_digest = storage.read_bytes_with_digest("reports/summary.json")
storage.write_bytes(
    "reports/summary.json",
    updated_content,
    expected_sha256=observed_digest,  # prevents overwriting a concurrent edit
)

If another tab or job changed an object after you read it, the write returns a conflict instead of overwriting the newer value. Read the current version, merge deliberately, and retry.

You can also use any installed Python library — pandas, numpy, matplotlib, altair, … — or install additional packages (e.g. plotly) straight from the notebook. See Python packages and egress below.

Ontology instances

Managed tables are the physical layer; the ontology is the semantic layer over them — object types like aircraft or supplier bound to a table, with identity columns that define what makes a row unique. scrydon.ontology lets a notebook read and write those object types by name, without knowing which table backs them:

from scrydon import ontology

# Which object types have data bound in this workspace?
ontology.list_types()          # DataFrame: ontology_slug, object_type_slug, label

# Read instances — each row is `id` (the instance's canonical key) plus one
# column per property. Masking, row filters, and clearance apply exactly as
# they do for the underlying table.
df = ontology.read("aircraft", limit=1_000)
df = ontology.read(
    "aircraft",
    where=[{"property": "status", "op": "eq", "value": "active"}],  # AND-combined, max 8
)

# Write instances back — an upsert keyed on the object type's identity columns.
# Existing instances are updated in place; new ones are inserted. Don't include
# the `id` column — it's computed server-side from the identity columns.
ontology.write("aircraft", df.drop(columns=["id"]))

A write goes through the same governed write path as tables.write — the platform resolves the bound managed table, applies the classification cap and mask round-trip guard, and audits the write under your user. The rows appear in the Analytics table preview and in the ontology instance graph immediately.

Identity columns are required to write. Upserting instances needs the object type's binding to declare identityColumns in the Ontology Workbench — that's what tells the platform which existing rows to update. Without them, ontology.write raises OntologyIdentityError. Reads work regardless.

Knowledge search and LLM helpers

The same session token also powers governed knowledge-base search and LLM completions — no API keys to manage, and the platform's data-loss-prevention scanning, billing, and audit all apply automatically:

from scrydon import knowledge, llm, embed

# Semantic search over the knowledge bases in this workspace environment.
hits = knowledge.search("What is the Q3 maintenance schedule?", top_k=5)
for hit in hits:
    print(round(hit["score"], 2), hit["content"][:100])

# Restrict to one knowledge base by id
hits = knowledge.search("supplier onboarding", collection="kb-id-here")

# LLM completion through your organization's default LLM integration.
answer = llm.complete("Summarize these findings in two sentences: ...")

# Full transcript + response metadata
result = llm.complete(
    messages=[
        {"role": "system", "content": "You are a terse analyst."},
        {"role": "user", "content": "Why partition large tables?"},
    ],
    detail=True,
)
print(result["content"], result.get("tokens"))

# Embeddings through your organization's embedding integration.
vectors = embed(
    ["the quick brown fox", "lorem ipsum"],
    model="openai/text-embedding-3-small",
)
print(len(vectors), len(vectors[0]))

All platform capabilities

Every platform AI capability is available through the governed /api/platform/v1/** operation family behind simple helpers. Each call runs through your organization's integrations with credentials, DLP, billing, and audit applied server-side; the notebook never sees a vendor API key:

from scrydon import stt, ocr, tts, image, video, moderation

# Transcribe audio — pass a file path or raw bytes.
transcript = stt.transcribe("interview.mp3", language="en")

# Extract text from a document (PDF / image).
text = ocr.extract("scan.pdf")
result = ocr.extract(pdf_bytes, mime_type="application/pdf", detail=True)

# Synthesize speech — returns decoded audio bytes.
audio = tts.synthesize("The pipeline finished successfully.", voice="alloy")
with open("status.mp3", "wb") as f:
    f.write(audio)

# Generate images — entries carry a URL and/or inline base64, per vendor.
images = image.generate("A schematic of an Iceberg table layout", n=2)

# Generate video — returns a URL or inline bytes, per vendor.
clip = video.generate("A drone flyover of a container port", duration=5)

# Classify text for content-safety categories.
# Each result carries flagged, categories, categoryScores, and orgVerdict.
results = moderation.moderate("Text to classify for harmful content.")
for r in results:
    if r["orgVerdict"] == "fail":
        print("Policy fail:", r.get("triggeredCategories", []))

Each helper accepts an optional model="vendor/model" override and raises a clear <Capability>NotConfiguredError when your organization has no matching integration — an org admin can enable one under Settings → Platform → Extensions. Binary inputs (audio, documents) accept a file path (MIME type inferred from the extension) or raw bytes, and are capped at the platform upload limit.

Web search and integration tools

The token also reaches the platform's governed web search and your organization's enabled integration tools:

from scrydon import search, tools

# Web search through the org's configured search integration.
for hit in search.web("Apache Iceberg snapshot expiry", max_results=5):
    print(hit["title"], hit["url"])

# Discover the org's enabled integration tools (name, description, input schema).
for tool in tools.list("send a message"):
    print(tool["id"], "-", tool["description"])

# Run one — parameters are validated server-side against the tool's schema.
output = tools.run(
    "org:acme:slack:send-message",
    {"channel": "#alerts", "text": "Pipeline finished."},
)

tools.run executes through the same governed bridge as workflows and chat: organization policy, server-side credential resolution, DLP, and audit all apply, and vendor code runs only inside the platform's integration sandbox. search.web accepts an optional provider= to pick a specific configured search integration (credentials still resolve server-side).

Governance notes:

  • Search results respect your clearance and workspace membership — the check runs on every call, so access changes take effect immediately, not at token expiry.
  • llm.complete uses the organization's default LLM integration (or pass model="vendor/model" to pick an enabled one). If none is configured, you'll get a clear LlmNotConfiguredError — an org admin can enable one under Settings → Platform → Extensions.
  • embed requires an explicit model (unlike llm.complete) — embedding dimensions must match wherever you store the vectors, so there's no silent default. If no embedding integration is configured, you'll get an EmbeddingNotConfiguredError.
  • Which capabilities and tools actually work is decided by your organization's enabled integrations — the notebook token only says you may ask. A missing integration returns a typed ...NotConfiguredError, never a silent fallback.

Notebooks from packs

Installed packs can ship ready-made notebooks. On Analytics → Notebooks, use From pack to browse notebooks shipped by your organization's installed packs and add one to the current workspace — it becomes a normal workspace notebook you can open, edit, and re-sync like any other.

Creating and uploading notebooks

Analytics → Notebooks → New Notebook creates a fresh notebook from a template — or uploads an existing marimo .py file (use Upload .py file in the same dialog).

Ready-made demo notebooks live in the scrydon/packs repository under notebooks/: a lakehouse quickstart, a publish-a-dataset walkthrough, and a governed write-back demo. Download a file, upload it into your workspace, and run it top to bottom.

Reactivity

Marimo's reactivity is what makes it useful here. Change a parameter at the top of the notebook (e.g. the table name, the date range, the filter), and every dependent cell re-runs. No stale state, no "run all cells" rituals.

Sharing

Notebooks are workspace-scoped. Sharing a notebook with another workspace member is a one-click op — they get a read-only view that re-runs in their own session, with their own masks applied.

Notebooks do not export raw data. A user with low clearance who opens a notebook produced by a higher-clearance user re-runs the queries under their own identity — they don't see the high-clearance user's cached results.

What they don't do

  • Marimo notebooks are not a replacement for the workflow engine. They're for analysis, not for production triggers.
  • Pipeline runs use fresh, single-use isolated compute and never reuse variables or files from your interactive session. For a stable schedule or production trigger, call that job from a workflow or automation.
  • They don't expose a public HTTP API. They're a UI surface.

Python packages and egress

The notebook runtime ships with a pinned base environment and the scrydon helper library. Everything else a notebook uses is declared in the notebook itself, in its PEP 723 script metadata block, and reconciled with uv before the kernel is marked ready:

# /// script
# requires-python = ">=3.12"
# dependencies = ["marimo", "pandas", "plotly", "scrydon"]
# ///

New notebooks are created with this block already in place.

Adding a package

Use any of Marimo's install affordances — they all do the same governed thing:

  • the Install button on a ModuleNotFoundError banner,
  • the Packages panel in the left sidebar,
  • the install link beside a missing import in a cell's output.

Scrydon adds the package to the notebook's dependency block, resolves and installs it, and saves the notebook — in that order. If the install fails, the notebook is left exactly as it was and the reason is shown. On success, re-run the cell to pick the new module up.

Because the package is recorded in the notebook source, it survives a reload, a replacement runtime, and a single-use pipeline job. There is no such thing as a package that is installed but undeclared.

Platform-managed packages

marimo and scrydon are installed by the platform from verified artifacts. You can leave them listed in the dependency block — new notebooks do — but a version constraint or extra on either is ignored rather than honoured, because the platform's own build is what gets installed either way. Removing them from the block is refused.

Notebooks written before this behaviour shipped may carry an auto-generated pin such as marimo>=0.24.0. It is ignored on the next preparation; you do not need to edit anything.

Restrictions

A declared dependency must be a plain requirement — a name, optional extras, and an optional version specifier. Direct references (package @ https://…, git+https://…, a local path, -e) are refused: they are not reproducible from the notebook source, which is the whole point of declaring them.

Packages that ship in the runtime's base image (pandas, numpy, and Marimo's own dependencies) are pinned by the platform. Declaring one is harmless, but asking to upgrade it does not move it off the pinned version.

Dependency reconciliation requires approved package egress. For public PyPI, pypi.org and files.pythonhosted.org must be allowed by your organisation's egress policy. A denied host fails preparation before the requested cell runs.

Connection or dependency preparation fails

  • Isolation unavailable — no qualified compute tier can satisfy your organisation's policy. Ask an operator to restore the Runtime Plane or Kata/KVM capacity; Scrydon does not fall back to the Analytics host.
  • Package egress denied — the package index or download host is absent from the egress allowlist.
  • A package would not install — the reason is shown where you asked for it. No compatible version could be resolved means no distribution matches this runtime's Python version and architecture; The notebook's dependency block is invalid means an entry in the block is not a plain requirement; The notebook changed means another tab saved first — reload and try again.
  • Preparation timed out or failed — choose Retry. Retry creates a new fenced generation and cleans up the failed one; it does not replay a cell that may already have crossed the execution boundary.
  • Source conflict — another tab or job saved a newer notebook version. Reload, merge your edits, and save again.
  • Stale connection after replacement or idle culling — reconnect. The old lease cannot attach to the replacement runtime.

Remedies for blocked dependency reconciliation:

  1. Ask an IT admin to enable egress. An org admin can open Settings → Governance → Egress and either enable Allow Scrydon default traffic (covers PyPI: pypi.org and files.pythonhosted.org) or add a custom domain to the allowlist. See the Notebook egress governance guide. A changed policy replaces the runtime before the next execution.

  2. Upload the package manually. A future release will let you upload Python wheel files directly to the platform, making them available without any egress. This is the path for completely air-gapped deployments. (This capability is not yet available — check the release notes for when it ships.)

If Allow Scrydon default traffic is enabled, packages still need a compatible distribution for the runtime's Python version and architecture. Resolver details stay in operator logs; the notebook receives a stable, safe failure reason.

Maps and other browser-rendered resources

Some libraries render output that fetches assets from the browser rather than the kernel — for example a Plotly map (go.Scattermap) loads its basemap tiles client-side. Those requests are governed by the notebook document's Content-Security-Policy, not the sandbox egress allowlist, so allowing a host for package installs does not by itself let a map's tiles load.

  • Basemaps work out of the box when Allow Scrydon default traffic is enabled. The platform's sanctioned basemap (CARTO) is permitted for notebook output, so use a token-free CARTO style — style="carto-positron" or style="carto-darkmatter" — and the map renders. If your org has opted out of Scrydon default traffic, no basemap is permitted and the map area stays blank.
  • Any other host — including style="open-street-map" — is an explicit operator opt-in. OpenStreetMap's public tile CDN is a separate third party that observes each viewer's viewport, so it is not part of the sanctioned defaults. An operator can allow it: in Settings → Governance → Egress, add the host (tile.openstreetmap.org) and turn on Browser resources for that entry. Adding a host without that switch allows only the kernel to reach it, which is why a map can stay blank even though the host is on the allowlist. See Notebook egress governance.
  • The notebook tells you when this happens. If a resource is refused, a notice appears above the notebook naming the exact host — you do not have to open the browser console to find out why a map is blank. It only appears for resources an administrator can allow (images and data fetches); a blocked script is refused by design and is never presented as something to request.
  • Tiles are not the only case. Any output that pulls images, fonts, or data straight from an external host in the browser is subject to the same document policy. Keep notebook output self-contained, or use the sanctioned basemap, unless an operator has widened the policy.
On this page

On this page