Shipping a tutorial
A package can teach its own blocks. Ship one or more tutorials and they appear in the user's Learning Center alongside the ones core ships, grouped under your package's name.
A tutorial is not a document. Each one bootstraps a real project with real data, and a step is complete when the backend can see it happened — a node exists, a run succeeded, a type registered, a file landed. A reader who finishes your tutorial is holding a working project, not a page they scrolled past.
Two paths, and you probably want the first
| You write | When | Needs Python? |
|---|---|---|
A tutorial.yaml manifest |
The steps are a fixed sequence, each judged by something the core vocabulary can express | No |
| A driver class | The logic depends on what the reader did, or a step is judged by something the vocabulary has no term for | Yes |
Start with a manifest. Reach for a driver only when a manifest genuinely cannot say what you mean — and even then, a driver can still defer most of its steps to the manifest's vocabulary.
The manifest path
Directory layout
A tutorial is a directory holding a tutorial.yaml and an assets/ tree. Put
them under one parent directory in your package:
src/scistudio_blocks_spectroscopy/
├── __init__.py
├── tutorials/
│ ├── __init__.py
│ ├── baseline-correction/
│ │ ├── tutorial.yaml
│ │ └── assets/
│ │ ├── data/example_spectrum.csv
│ │ └── code/custom_baseline.py
│ └── peak-fitting/
│ ├── tutorial.yaml
│ └── assets/
└── ...
Declare the entry point
The scistudio.tutorials group names the parent package, and core scans its
directory for tutorial subdirectories:
[project.entry-points."scistudio.tutorials"]
scistudio_blocks_spectroscopy = "scistudio_blocks_spectroscopy.tutorials"
Core resolves this to a directory from your distribution's metadata and never imports it to list the catalogue. Opening the Learning Center reads manifests and nothing else, so a package with a broken tutorial breaks that one entry rather than the catalogue.
Ship the directory as package data, since the tree is deliberately not importable:
[tool.setuptools.package-data]
scistudio_blocks_spectroscopy = ["tutorials/**/*"]
Write the manifest
manifest_version, id, title, and summary are required; everything else is
optional. The published schema is
scistudio/tutorials/schema/tutorial.schema.json — the contract, and the thing
to validate against.
manifest_version: 1
id: baseline-correction
title: Correcting a baseline
summary: >-
Load a spectrum, correct its baseline, and see the difference on a plot.
order: 1
requires:
scistudio: ">=0.3.4"
packages: ["scistudio-blocks-spectroscopy"]
agent: false
bootstrap:
project_name: Baseline correction
do:
- copy:
source: assets/data
destination: data/raw
steps:
- id: add-the-loader
title: Load the spectrum
route_to: canvas
say: Drag a Spectrum Loader onto the canvas and point it at the raw file.
done_when:
node_exists:
block_type: SpectrumLoader
- id: correct-it
title: Correct the baseline
say: Add a BaselineCorrection block and connect the loader to it.
done_when:
all:
- node_exists:
block_type: BaselineCorrection
- edge_exists:
source_block_type: SpectrumLoader
target_block_type: BaselineCorrection
- id: run-it
title: Run it
say: Press Run. The corrected spectrum lands on the output port.
done_when:
run_succeeded: {}
requires is what makes an entry honest: a tutorial whose requirements are unmet
is listed as unavailable, saying which requirement is missing, rather than
failing once the reader is inside it.
Conditions
done_when takes one vocabulary term, or an all / any of them. The terms are
core-owned and the single declaration of them is
scistudio.tutorials.VOCABULARY; the schema deliberately does not restate the
names. They cover nodes and edges, node configuration, runs and ports, registered
blocks/types/previewers, plots, files, git branches, library contents, and
frontend interactions.
Steps may write files into the project at any point, not only at bootstrap, and the files are on disk before the step's text is readable — a step that says "we wrote this block for you" cannot be read before the block exists.
The driver path
Implement TutorialDriver when the manifest cannot express the tutorial's logic.
Name the class from the manifest:
manifest_version: 1
id: adaptive-fitting
title: Fitting until it converges
summary: A tutorial whose next step depends on how the fit went.
driver: scistudio_blocks_spectroscopy.tutorials.adaptive:FittingDriver
Driver is a core/package-tier field
driver is accepted only from core and package tutorials. A user-level or
project-level manifest declaring it is rejected, naming the field and the
tier restriction.
The four questions
The protocol is four methods and stays four. Everything comes from the canonical root:
from scistudio.tutorials import (
Condition, DriverContext, ProductState, StepView, WriteAction, evaluate,
)
class FittingDriver:
def __init__(self, manifest, key):
self._steps = list(manifest.steps)
def step_view(self, context: DriverContext) -> StepView:
step = self._step(context.step_id)
return StepView(
id=step.id,
index=self._steps.index(step),
total=len(self._steps),
title=step.title,
say=step.say,
)
def is_satisfied(self, context: DriverContext, product: ProductState) -> bool:
step = self._step(context.step_id)
if step.id == "converged":
return self._residual_below_threshold(product) # no term for this
return step.done_when is None or evaluate(step.done_when, product)
def entry_actions(self, context: DriverContext) -> tuple[WriteAction, ...]:
return self._step(context.step_id).actions
def advance(self, context: DriverContext) -> str | None:
if context.step_id is None:
return self._steps[0].id # "the step after nothing"
nxt = self._steps.index(self._step(context.step_id)) + 1
return self._steps[nxt].id if nxt < len(self._steps) else None
Note is_satisfied: the driver implements the one condition the vocabulary
cannot express and hands every other step to evaluate. That is the intended
shape — a driver is an escape hatch for the hard step, not a reason to reimplement
the easy ones.
What a driver does not do
Three constraints, each of which removes work rather than adding it:
- Core owns rendering. Whatever
step_viewreturns is reduced to a plainStepViewat the boundary; extra attributes and extra mapping keys are dropped. A driver cannot introduce a display primitive, ship a frontend asset, or address a surface the manifest format cannot address — and correspondingly never has to describe one. This is also what makes a package driver and a manifest tutorial indistinguishable to everything downstream. - The session holds the cursor. A driver is asked about the step its
DriverContextnames, not about "its" current step. It persists nothing and survives a backend restart with no state of its own. - The context is not the session. A driver reads position and location and cannot advance, end, or start a session.
Optional: declaring conditions
Implement DeclaresConditions as well and the session can skip re-evaluating a
step on events that could not affect it. It changes how often is_satisfied is
called, never what the reader sees.
def condition(self, context: DriverContext) -> Condition | None:
step = self._step(context.step_id)
return step.done_when # None for a step the vocabulary cannot express
Failure is contained
Your driver is imported only when a reader starts that tutorial. An import failure ends that one session, naming the tutorial, and leaves every other tutorial listed and startable.
Stability
scistudio.tutorials is a canonical public root; its __all__ is this authoring
surface and nothing else. Everything else in the package stays importable by deep
path and carries no promise — do not build against it.
The surface is provisional as of 0.3.4: the condition vocabulary and the
action set are still settling, so they may change in a minor release with a
changelog note. Pin scistudio accordingly.
The manifest format versions itself separately through manifest_version. A
manifest declaring a version newer than the installed core is reported as needing
a newer SciStudio rather than as malformed.
Where to look next
| For | See |
|---|---|
| Every public symbol, with signatures and tiers | the scistudio.tutorials page of the API reference |
| The manifest contract | scistudio/tutorials/schema/tutorial.schema.json |
| A complete worked manifest | core's welcome-to-scistudio tutorial in the SciStudio repository |
| Entry points, packaging, versioning | publishing.md |