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.
What they're for
| Use case | Why marimo |
|---|---|
| Ad-hoc analysis on a managed table | No need to export — query the table from Python with the user's masks already applied |
| Build a one-off report | Reactive cells make iterating quick |
| Sketch out a feature engineering pipeline before turning it into a workflow | Same data surface, same governance |
| Run model evaluations against historical data | Notebooks 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:
- When you open a notebook, Analytics mints a short-lived, user-scoped session token (default 8 hours) and hands it to your notebook session automatically. There are no keys to copy and nothing to configure.
- Reads apply your column masks — the same policies the Analytics table preview enforces.
- Writes re-check your workspace membership on every call and land in the audit trail under your user.
- The notebook runtime never holds database credentials or service identity — the session token is the only credential notebook code ever sees, and it cannot reach the raw Iceberg catalog.
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"]).
If a read or write fails with a credentials error, re-open the notebook from Analytics → Notebooks — that refreshes your session token.
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 the same way — one governed endpoint (POST /api/v1/capabilities/execute) 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 → Integrations. 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.completeuses the organization's default LLM integration (or passmodel="vendor/model"to pick an enabled one). If none is configured, you'll get a clearLlmNotConfiguredError— an org admin can enable one under Settings → Integrations.embedrequires an explicitmodel(unlikellm.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 anEmbeddingNotConfiguredError.- 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.
- They don't have a stable schedule / cron. If you need that, build a workflow or an automation.
- They don't expose a public HTTP API. They're a UI surface.
Python packages and egress
The notebook runtime ships with a base set of packages already installed — pandas, numpy, scipy, duckdb, matplotlib, altair, pyarrow, polars, and the scrydon helper library. These are available without any external network access.
For packages that are not pre-installed (for example plotly), import them and let marimo install them for you:
- Add an
import plotly(or whatever you need) to a cell and run it. - marimo detects the missing package and shows a Missing packages banner with an Install button. Click it — marimo runs
pip installfor you and re-runs the affected cells once the install finishes.
Installed packages live for the life of the notebook runtime. They are not persistent: a platform redeploy or a runtime restart resets to the base set, so a notebook that relies on an extra package should keep the import cell that triggers the install (re-running it re-installs on demand).
Installing requires egress to PyPI. The Install button downloads from the public Python Package Index, so pypi.org and files.pythonhosted.org must be reachable from your deployment. If they aren't, the install fails — see below.
The Install button is missing, or an install fails
- No banner / no Install button at all is unexpected on a current build — it means the notebook runtime isn't reporting an isolated environment. Report it to your platform administrator (it should not happen on a supported deployment).
- Install fails with a connection /
Network is unreachableerror — PyPI is not in your deployment's egress allowlist.
Two remedies for a blocked install:
-
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.organdfiles.pythonhosted.org) or add a custom domain to the allowlist. See the Notebook egress governance guide for step-by-step instructions. Changes take effect on the next notebook run. -
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 your deployment has Allow Scrydon default traffic enabled, installing any package published to the public Python Package Index should work. If a specific package fails even with defaults enabled, check that the package name is correct and that it is available on PyPI for your Python version.
Related
- Architecture → Analytics stack — where marimo sits in the cluster.
- Managed tables — what the notebooks read from.
- Classification & masking — what governance applies.
- Notebook egress governance — configure which external hosts notebooks can reach.
- Connect an external engine — your notebook and a Spark or Trino cluster read the same governed Iceberg catalog; an engine token is all they need.