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.
1 · Pipeline 2 · Workflow 3 · Execute ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ build a REUSABLE │ compile │ wire real DATA │ compile │ run it, per │ │ processing/model │────────▶│ into one or more │────────▶│ subject, and see │ │ unit │ block │ compiled units │ │ metrics + graphs │ └──────────────────┘ └──────────────────┘ └──────────────────┘ no files here Select Files lives here results land here
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:
Conv1d(k=3) → ReLU ─┐
Conv1d(k=5) → ReLU ─┼─▶ Model Concat ─▶ Linear ─▶ Trainer.Model
Conv1d(k=7) → ReLU ─┘
each branch sees the SAME inputModel 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.
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.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 Predictions + True Labels into a Confusion Matrix Graph and Training History into a Data Graph to see the run, not just its number.Compile
Select Files ─X──▶ BCILattice Trainer . Dataset
└Y──▶ BCILattice Trainer . Labels
your model ──────▶ BCILattice Trainer . Model
Trainer . Metrics ──────────▶ Output Block
Trainer . Predictions ──────▶ Confusion Matrix Graph . y_pred
Trainer . True Labels ──────▶ Confusion Matrix Graph . y_true
Trainer . Training History ─▶ Data Graph . DataWhat 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.
Pipeline tab — "EEG Decoder"
┌──────────────────────────────────────────────────────────────────┐
│ [Input Block] │
│ port_name: Signal ──┐ │
│ ├──▶ [Channel Selection] ──▶ [BCILattice │
│ [Input Block] │ Trainer] ──┐ │
│ port_name: Labels ──┘ │ │
│ [Output Block] ◀──────┘ │
│ port_name: Model │
└──────────────────────────────────────────────────────────────────┘
│ Compile
▼
Workflow tab — the same pipeline, now one block
┌──────────────────────────────────────────────────────────────────┐
│ [Select Files] ┌─────────────────┐ │
│ modality: EEG │ EEG Decoder │ │
│ X ───────────────────────▶│ Signal Model │──▶ [Output │
│ Y ───────────────────────▶│ Labels │ Block] │
│ └─────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Signal / Labels / Model are the port_names you set above.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, BCI Cross Validation, BCI Epoching. |
| 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.
Per-subject vs pooled
By default a run fans out one job per subject — each subject is trained and evaluated independently, which is usually what you want for a within-subject BCI result.
Cross-subject validation needs pooled mode
subject_scope to all to collapse every selected subject into a single run — then a subject-wise split or LOSO strategy has real subject groups to work with.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.
Standard Preprocessing starter
When you create a new Workflow tab from the + tab, you are asked whether to start Blank or from Standard Preprocessing — the bundled community EEG preprocessing pipeline. Pick Blank when you know what you are building; pick Standard Preprocessing when you want a known-good chain to adapt.
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 subject_scope to all. |
| 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. |
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.