Using the ML Suite
How Pipelines and Workflows fit together — build a reusable pipeline, drop it into a workflow, and run it against real recordings.
The Three Steps
The ML Suite sidebar is numbered for a reason — it is a pipeline of its own, and each step consumes what the one before it produced.
Where everything is
Every path below starts from the main sidebar on the left of the app.
| What | Where |
|---|---|
| Build a pipeline | ML Suite → 1 · Pipeline |
| Build a workflow | ML Suite → 2 · Workflow |
| Run it | ML Suite → 3 · Execute |
| Block palette | Left panel of either canvas |
| Compile | Canvas toolbar, the ⚡ Compile button |
| Build with AI | Bottom of the block palette |
| Import from Community | Bottom of the block palette |
| New tab / new pipeline | The “+” tab at the end of the tab bar |
| Rename a tab | Double-click the tab |
| Block settings | Select a block — the inspector opens on the right |
| Load recordings first | Load Dataset (sidebar), before either canvas |
| Results and reports | Results, and Experiment Hub for run history |
Load your data first
Which canvas do I want?
The split confuses people at first, and the rule is simpler than it looks:
| Question | Answer |
|---|---|
| Am I defining what to do to some data? | Pipeline |
| Am I choosing which recordings it runs on? | Workflow |
| Do I want this reusable across several studies? | Pipeline — compile it once, drop it in many workflows |
| Do I just want to run something once, now? | Workflow alone is enough |
| Is my model a single block (KNN, SVM, Random Forest)? | Workflow alone — a Pipeline adds nothing |
| Is it a layer stack, parallel branches, or preprocessing + model? | Pipeline — that is what it is for |
| I already built it — or imported it — on the wrong one. | Right-click the tab → Move to…. See Moving a tab to the other canvas. |
Start in the Workflow
Select Files → your model → BCILattice Trainer → Output Block straight onto the Workflow canvas. Four blocks, one compile.Use a Pipeline for complex structure — a deep-learning layer stack, parallel branches, a preprocessing chain feeding a model — especially when you want that structure as one reusable block across several workflows. Building a one-block model as a Pipeline just adds a second canvas and a second compile for no gain.
1 · Pipeline
A Pipeline is a reusable unit: a named box with named input and output ports. It deliberately has no file picker — it does not know or care which recordings it will see. That is what makes it reusable.
Building a pipeline
Mark the inputs — if it takes any
port_name (for example Signal and Labels). The port drawn on the canvas is labelled with that name, and it is exactly the port you will wire to later — so name them for what they carry, not In_1.A pipeline that only defines a model needs none. Layer blocks do not take data on the canvas — the trainer feeds them — so skip this step entirely and the compiled block will have no input port to wire.
Do the work
Mark the output
port_name too. A pipeline needs exactly one final output — see below.Compile
Building a model: the wires are forward
If you know PyTorch, the mapping is one table and everything follows from it:
| PyTorch | ML Suite |
|---|---|
| __init__ | the layer blocks you drop |
| forward(x) | the wires between them |
| x | whatever the trainer feeds in |
| return | the last block before the trainer’s Model port |
| the training loop | the BCILattice Trainer block |
Chain layer blocks and they compose into one nn.Module, in the order you wired them. Recurrent layers chain too — LSTM → Flatten → Linear works, even though nn.Sequential cannot express it.
You never wire data into a layer. A layer with nothing feeding it is a root and receives the trainer's input. That is what makes parallel branches simply a matter of drawing them:
Model Concat and Model Add (palette: Model Structure) apply every wired Branch N to the same input. Concat joins the outputs; Add sums them, which is how you draw a skip connection — put an Identity layer on the other branch.
These are not the Data Routing blocks
A model block has no data inputs — leave it that way
Model port. The BCILattice Trainer is what puts data through it: it owns the train/test split and calls fit itself.If a model block is showing
X and y ports, its Method is set to fit — that block is a constructor and a fit call in one. Either set it back to (none — just build the model) and let the trainer do the fitting, or, if you meant it to fit there, feed those ports with Trainer Trigger blocks: they hand over the training split, so the node never sees the rows it will be scored against.See what you built
nn.Module — the exact object the trainer receives, generated from the constructed model rather than from a second reading of the graph. Save as .py exports it as a standalone file that imports nothing from BCILattice, so a model built from blocks can always leave as ordinary code.What compiling checks
Compile is validation, not just a save. It refuses a graph that:
- has no input node — at least one
Input Blockis required, unless the pipeline only defines a model, which takes no data; - does not have exactly one final output node. Zero is ambiguous, and so is two;
- contains a cycle — the graph has to have a running order;
- has no model or trainer node, matched against the catalog by type or category, never by guessing at names;
- has two wires landing on the same input port. The canvas already refuses this while you draw it, so this only catches a graph saved before that guard existed — the message names the block and offers the fix (a Model Collection when the port is a trainer's
Model).
Blocking errors stop the compile and the offending nodes are marked red on the canvas. Warnings are shown in a dialog and you can choose to proceed.
Compiling and publishing
A successful compile registers the pipeline under its tab name and a stable internal id, and it appears in the Workflow palette under Model/Pipelines (Compiled). Recompile whenever you change the pipeline — a workflow that already embeds it keeps using the last compiled version until you do, which is deliberate: editing a pipeline never silently changes a workflow mid-experiment.
2 · Workflow
A Workflow is where abstract processing meets actual recordings. This canvas has Select Files, and it is the only one that does.
Building a workflow
Load the data
data_source), the session and the subjects. It emits X and Y, plus stage-specific ports.Add your model
Add the trainer — this is what produces metrics
Dataset / Labels from Select Files, and the model on Model. A model on its own never reports anything — the trainer owns the split, the fit and the scoring.Cross-validating? Wire a Validation Scheme into the trainer's
Val/Eval input (k-fold, leave-one-subject-out and more). Left empty, the port reads one hold-out split, which is what the trainer does without one. There is no separate cross-validation block.Training on some sessions and testing on others? Wire an Evaluation Protocol into the same
Val/Eval input instead: train on Session 1 and test on the rest, run each session in turn (indoor → outdoor and back), or name the subjects that train and the ones that test. The results sit in the Execute tabs you already read, and every table puts one evaluation per row and one metric per column: Per Pipeline gives Within-session and Cross-session as mean ± SD across subjects with a Contrasts table under it, Per Subject lists every train → test pair with its Evaluation, Train and Test, Detailed gives one metric's train score, test score, gap, CI, chance and p per pair, Per Class breaks each pair down by class, Statistics tests Within-session vs Cross-session paired on subject, and Graphs holds the train × test matrix and one confusion matrix per pair. An Evaluation filter above the long tables shows one evaluation at a time. Each session's trained model is kept under its own name for export (“CNN · trained on indoor”). The Experiment Hub and Model Analysis show them too; see Validation & Reporting for how every page keeps runs validated differently apart.Comparing more than one model? The
Model port takes one wire — drop a second model onto it and the canvas offers to insert a Model Collection instead of rejecting the wire outright. Wire every model into it and its one Models output into the trainer: each subject's recordings load once, every model trains on them, and each model gets its own row of results. Everything for working with several models — training them together, keeping the best, testing whether a difference is real — is in one palette category, Multiple Models, just below Training; hover it for a “which one do I need?” list, or see Multiple Models.Wire the results and finish
Metrics to an Output Block — every workflow ends in exactly one of those (or a Graph Output / Graph Endpoint). Tap Test Predictions + Test Labels into a Confusion Matrix Graph and Training History into a Data Graph to see the run, not just its number.Compile
What compiling checks
The same rules as a pipeline, with a wider set of accepted endpoints: the input may be an Input Block or Select Files, and the single final output may be an Output Block, a Graph Output or a Graph Endpoint.
Only terminal nodes compete for the output slot
3 · Combining the two
This is the part worth understanding properly, because everything else follows from it.
The port contract
One block, one port
port_name.…and no port at all when there is no input
When a pipeline does take data, leaving one of its ports unwired is a compile error that names the port — the run will not silently proceed on whatever data happened to be in scope.
Which trainer port a pipeline block feeds follows from what comes out of it: a model goes to Model, processed data goes to Dataset.
So the shape of the block you drop into a Workflow is decided entirely by how many Input/Output Blocks you put in the pipeline, and what you named them. Two inputs and one output in the pipeline gives you a block with two inputs and one output in the Workflow.
Give every Input Block a distinct name. Two blocks sharing one name compile to two identically named ports, which cannot be told apart when wiring.
A worked example
A preprocessing-plus-decoder pipeline that takes a signal and labels, and returns a trained model.
Note what did not happen: the pipeline never mentioned EEG, a session, or a subject. Point a second Select Files at fNIRS recordings and the same compiled block runs against those instead.
Moving a tab to the other canvas
You imported someone's published work, it opened in the Workflow tab, and it belongs in the Pipeline one — or the reverse. Right-click the tab and choose Move to Pipeline… or Move to Workflow….
This is not a rename. The two canvases carry different palettes and enforce different rules, so some blocks change shape and some cannot come with you. Nothing happens until a report has shown you every block, grouped by what happens to it, with the reason attached:
| Block | What happens |
|---|---|
Select Files → Pipeline | Becomes one Input Block per port you actually wired, so every connection survives. The subject scope and cohort filters come with it; the chosen files do not — a pipeline is fed by whatever runs it. |
Input Block → Workflow | Moves as it is — a workflow called by Sub-Workflow is legitimately fed by its caller. No Select Files is added for you; add one yourself before running it from Execute. |
| Blocks the other palette does not have | Removed, each one named with why. Going to a Pipeline that means Model Set, Sub-Workflow and any compiled pipeline; going to a Workflow it means the Pipeline-only families — BCI Decoding, Riemannian Geometry, BCI Augmentation. |
| Everything else | Moves unchanged. |
It will drop things, but it will never invent them
The source tab closes on a clean move. The moment anything has to be dropped, Keep the original open comes up already ticked, so a lossy conversion never costs you the work you started from.
If a lot is being dropped, you probably want a compiled pipeline instead
Saving, renaming, reopening
- Saving a workflow writes a
.workflowfile and co-saves every pipeline it references as a.pipelinefile in the same folder, so the pair travels together. Keep the referenced pipelines open in the Pipeline tab when you save, or they cannot be bundled. - Loading a workflow reads those
.pipelinefiles back, recompiles them, and marks any block red whose pipeline file is missing or failed to compile. - References are stored by stable id, not by name, so renaming a pipeline does not break a workflow that embeds it.
- Both canvases are saved into the study itself, so reopening a study restores every pipeline and workflow tab as you left them.
4 · Execute
The Execute tab lists compiled workflows for the active study. Pick one, pick your subjects, and run. Metrics, training history, confusion matrices and any graphs your blocks produced land in the results tabs.
Save knowledge of this run (trained models), at the top of the page, decides whether this run's trained models are kept for good. Unticked, only the results are kept once a newer run executes; the current run keeps its models until then. When the run finishes, its validation design appears beside the run id (for example Validation: Evaluation Protocol, across sessions).
Per-subject vs pooled, and comparing models
Select Files's subject_scope decides how the recordings you selected become runs. Subjects and sessions are independent axes, and each can be kept apart or pooled, which gives four modes:
| subject_scope | 20 subjects × 2 sessions produces |
|---|---|
| Per subject, per session | 40 runs, 40 scores — every recording on its own |
| Per subject, combined sessions (default) | 20 runs — one per subject, that subject's sessions pooled |
| Combined subjects, per session | 2 runs — one per session, across all subjects |
| Combined subjects, combined sessions | 1 run over everything selected |
There is deliberately no “auto”: which of these four a study wants is a design decision, not something to infer from how many files happen to be selected.
Cross-subject validation needs a pooled mode
subject_scope to Combined subjects, per session or Combined subjects, combined sessions so a subject-wise split or LOSO strategy has real subject groups to work with.The trainer can declare it too — and the two must agree
cohort_mode, plus Inherit from the data source (the default, which follows Select Files). Set both to different modes and the run refuses before training anything, naming both blocks — a per-subject score and a cross-subject one answer different questions from the same recordings, and there is no defensible way to pick one.Comparing several models: one run, one row per model
Models output into the trainer, and the subject fan-out above still applies — on top of it, the loop reads: subjects outside, models inside. Each subject's recordings load once, whichever subject_scope you picked, and every model trains on that same load. Every (subject × model) combination — or every (run × model) combination under a pooled mode — gets its own row in the Training dashboard, the Experiment Hub, Results and Model Analysis, named "<workflow> · <model>". This holds even when the collection sits inside a compiled Pipeline block embedded in the workflow.Free vs Researcher
One thing in the ML Suite depends on your plan, and it is worth being precise about what it is — because it gates a shortcut, not a capability.
Importing an Analysis stage
Select Files's data_source lets you train directly on a stage the Analysis suite already produced — channel-selected data, a dimensionality reduction, an extracted feature table, a time-frequency transform, a signal projection. That reuse is a Researcher feature.
| data_source | Plan |
|---|---|
| Filtered | Free — the band-passed recording, the one pre-analysis entry |
| ICA Cleaned Data | Researcher |
| Quality Checked Data | Researcher |
| Epochs | Researcher |
| Normalized Data | Researcher |
| Channel Selected Data | Researcher |
| Dim Reduced Data | Researcher |
| Features | Researcher |
| Time-Frequency (ERSP) | Researcher |
| Signal Projection (images) | Researcher |
On a Free plan the locked entries are refused when you pick one, and Compile also refuses a graph that reached a locked source another way — by loading a file, importing from the community, or generating with AI.
Rebuilding it yourself
Nothing is out of reach on Free
Filtered and wire the processing you need — the result is the same pipeline, built explicitly instead of inherited. Pipeline Recipes walks through each one with the exact ports and settings.| Instead of importing… | …wire this block |
|---|---|
| Channel Selected Data | Channel Selection |
| Signal Projection (images) | Signal Projection |
| Time-Frequency (ERSP) | Time-Frequency Features |
| Dim Reduced Data | PCA · FastICA · KernelPCA · TruncatedSVD |
| Features | SelectKBest · SelectPercentile · VarianceThreshold · RFE |
Building it in the canvas has a real advantage too: the processing is part of the pipeline, so it travels with it, recompiles with it, and is visible to anyone reading the graph — rather than being an out-of-band step someone has to know was run.
Faster starts
Three ways to avoid starting from a blank canvas. All three produce an ordinary editable graph — nothing they generate is locked, and nothing runs until you compile it yourself.
Build with AI
Where: the Build with AI button at the bottom of the block palette, on either canvas.
Describe what you want in plain language and it lays out a starting graph for the canvas you are on. It is catalog-aware: your description is matched against the live block catalog and the most relevant blocks are given to the model, so it builds from blocks that actually exist in your installation rather than inventing plausible names.
Pick a provider and model
Describe the pipeline
Review before you trust it
It produces a draft, not a finished pipeline
Import from Community
Where: the Import from Community button, directly below Build with AI.
Browses published community pipelines and workflows and imports one into your study, as a new tab or merged into the current canvas. Useful when someone has already solved the preprocessing you need. The same review applies — an imported graph is a starting point, and its parameters were tuned for someone else's recordings.
Starting a new tab
The + tab on either canvas offers three starts. New opens an empty graph with the one block it always needs — Select Files on a Workflow, an Input Block on a Pipeline. From existing makes an exact copy of a tab already open in the study. From community opens the community browser.
Replacing a block
Right-click a block → Replace with… and pick another type. The new block takes the same place, keeps settings with the same name and every wire whose port still exists; a wire with nowhere to land is removed and named. One Undo restores the original.
Layer sizes on auto
Chain PyTorch layers model_obj → model_obj and leave any size at 0. Before a run — or on right-click of the pipeline block in a Workflow → Auto-fill layer sizes… — input sizes are read from the Workflow's data, the last Linear is sized to the number of classes, and kernel_size defaults to 3. Widths cannot come from the data: if every one is on auto you are asked for the first and last (the ones between are optional). The values are written into the pipeline and recompiled, so this happens once.
Troubleshooting
| Message | What to do |
|---|---|
| Compilation failed: no input node | Add an Input Block (Pipeline) or a Select Files block (Workflow). |
| Exactly one final output node required | You have zero or two terminal nodes. Data Graph does not count — Output Block, Graph Output and Graph Endpoint do. |
| No model or trainer node | A pipeline has to actually train or apply something. Add a model block or a BCILattice Trainer. |
| Compiled pipeline not found | The pipeline was never compiled, or was compiled in a different study. Open its Pipeline tab and press Compile. |
| Pipeline file not found on load | The .pipeline file was not next to the .workflow file. Reopen both pipelines and re-save the workflow to bundle them. |
| Strategy needs at least 2 distinct groups | Cross-subject validation on single-subject data. Select more subjects and set Select Files' subject_scope to Combined subjects, per session or combined sessions. |
| This run's blocks disagree about how the recordings become runs | Select Files' subject_scope and a trainer's cohort_mode named two different modes. Set them to the same one, or leave the trainer on Inherit from the data source. |
| A graph did not draw | Read the Graph Error port on Data Graph / Graph Output — it carries the exact reason. |
| This workflow has no Select Files block | Shown after moving a pipeline to the Workflow canvas. Nothing in a pipeline says which recordings to use, so none is added for you — drop a Select Files in and wire it to the Input Blocks before running from Execute. |
| N block(s) end up with no connections at all | Shown before a move: those blocks fed off something the other palette does not have. Convert anyway and rewire them, or compile the pipeline and embed it as one block instead. |
| Multiple connections to the same input are not supported / N wire(s) on one input | Two models (or two anything) wired into one port. Delete the extra wire, or — for a trainer's Model port — let the canvas insert a Model Collection for you. |
| '…' trains N models through a Model Collection. Set 'collection_model' … | The saved graph trained several models. Set collection_model to the one to load (a package exported from one model's own results row already carries the answer and needs nothing set). |
Block-by-block reference: Custom Blocks for the everyday palette, Advanced Blocks for control flow, domain adaptation, continual learning and training primitives, and Extensions for turning a package we did not bundle — Cedalion, or anything on the curated shelf — into blocks on this same canvas.