DocsUsing the ML Suite
BCILattice Documentation

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.

v1.0BCINexus Platform · 2026-05-20[email protected]

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.

compilecompile1 · Pipelinebuild a reusableprocessing or model unit2 · Workflowwire real data into oneor more compiled units3 · Executerun it per subject, and seemetrics and graphsno files hereSelect Files lives hereresults land here

Where everything is

Every path below starts from the main sidebar on the left of the app.

WhatWhere
Build a pipelineML Suite → 1 · Pipeline
Build a workflowML Suite → 2 · Workflow
Run itML Suite → 3 · Execute
Block paletteLeft panel of either canvas
CompileCanvas toolbar, the ⚡ Compile button
Build with AIBottom of the block palette
Import from CommunityBottom of the block palette
New tab / new pipelineThe “+” tab at the end of the tab bar
Rename a tabDouble-click the tab
Block settingsSelect a block — the inspector opens on the right
Load recordings firstLoad Dataset (sidebar), before either canvas
Results and reportsResults, and Experiment Hub for run history

Load your data first

Both canvases read the subject and session lists from the Data Manager. If Select Files shows nothing to pick, go to Load Dataset and import the recordings before returning.

Which canvas do I want?

The split confuses people at first, and the rule is simpler than it looks:

QuestionAnswer
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

A Workflow does not require a Pipeline, and for a simple model you should skip the Pipeline canvas entirely: drop 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

1

Mark the inputs — if it takes any

Drop one Input Block for each data stream the pipeline accepts, and set its 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.
2

Do the work

Wire your processing between them — Channel Selection, Signal Projection, Time-Frequency Features, a model block, a BCILattice Trainer, whatever the pipeline is for. Anything in the palette is available here.
3

Mark the output

Finish with an Output Block, and set its port_name too. A pipeline needs exactly one final output — see below.
4

Compile

Press Compile. That validates the graph and, on success, publishes it as a single reusable block for the Workflow canvas.

Building a model: the wires are forward

If you know PyTorch, the mapping is one table and everything follows from it:

PyTorchML Suite
__init__the layer blocks you drop
forward(x)the wires between them
xwhatever the trainer feeds in
returnthe last block before the trainer’s Model port
the training loopthe 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:

Conv1dkernel_size: 3model_objmodel_objConv1dkernel_size: 5model_objmodel_objConv1dkernel_size: 7model_objmodel_objModel Concatdim: 1Branch 1Branch 2Branch 3model_objLinearmodel_objmodel_objBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation Report
The three Conv1d blocks have nothing wired into them, so each is a root and receives the trainer's input: every branch sees the same data. An activation (ReLU) goes between a Conv1d and its Branch port the same way, model_obj to model_obj.

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

Feature Concatenator, Parallel Fan-out and Ensemble Voter look similar and operate on arrays — they combine data or predictions between models. Model Concat and Model Add operate on models and build structure inside one network. A third kind again: Model Collection trains several independent models side by side on the same data and reports each of them separately — it does not fuse them into one network (that's Model Concat/Add) or combine their predictions into one (that's Ensemble Voter).

A model block has no data inputs — leave it that way

KNN, SVM, Random Forest, a Conv1d — none of them take data on the canvas. You drop the block, set its parameters, and wire its output to the trainer's 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

Right-click the canvas → View as PyTorch… prints the real 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 Block is 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

1

Load the data

Drop Select Files and choose the modality, the processing stage (data_source), the session and the subjects. It emits X and Y, plus stage-specific ports.
2

Add your model

Either drop the model blocks straight onto this canvas, or find your compiled pipeline in Model/Pipelines (Compiled). A compiled pipeline that takes data wires itself to the Select Files already on the canvas when you drop it; one that only defines a model has no input port to wire.
3

Add the trainer — this is what produces metrics

Drop a BCILattice Trainer and give it two different things: the data on 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.
4

Wire the results and finish

Send the trainer's 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.
5

Compile

Same button, same meaning. A compiled workflow is what the Execute tab can run.
Select Filesmodality: EEGXYChannelsFilesFile Pathsyour modelmodel_objBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation ReportOutput BlockDataData GraphDataGraphDataGraph FileGraph ErrorConfusion Matrix Graphy_truey_predGraph
Unfilled dots are ports left unwired. With a Validation Scheme wired, Test Labels and Test Predictions are out-of-fold, so the confusion matrix covers every trial.

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

Data Graph is a pass-through tap, not a terminal node, so you can use as many as you like without breaking the “exactly one final output” rule. Graph Output and Graph Endpoint are terminal — two of those in one graph is a compile error.

3 · Combining the two

This is the part worth understanding properly, because everything else follows from it.

The port contract

One block, one port

Each Input Block inside a pipeline becomes one input port on the compiled block. Each Output Block becomes one output port. The port takes its name from that block's port_name.

…and no port at all when there is no input

A pipeline that only defines a model consumes no data, so it compiles with no input ports even if you left an Input Block on the canvas. There is nothing to connect and nothing left dangling.

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 Blockport_name: SignalDataInput Blockport_name: LabelsDatayour model blockmodel_objChannel SelectionDataLabelsSelected DataSelected ChannelsScoresBCILattice TrainerDatasetModelLabelsVal/EvalAdditional MetricsCallbacksTrained ModelMetricsTraining HistoryProvenanceLogsTest LabelsTest PredictionsTest ProbabilitiesFold ScoresCV ReportEvaluation ReportOutput Blockport_name: ModelData
Compile, and on the Workflow tab the whole pipeline is one block:
WORKFLOW TABSelect Filesmodality: EEGXYChannelsFilesFile PathsEEG DecoderSignalLabelsModelOutput BlockData
Signal, Labels and Model are the port_names you set on the Input and Output Blocks 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:

BlockWhat happens
Select Files → PipelineBecomes 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 → WorkflowMoves 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 haveRemoved, 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 elseMoves unchanged.

It will drop things, but it will never invent them

Converting a workflow discards its file selection, because a pipeline has no file picker. Converting a pipeline the other way does not hand you a Select Files in return — nothing in a pipeline says which recordings to use, and a guessed cohort would give you a result rather than an error. You are told to add one instead.

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

A pipeline full of CSP, Covariances or Tangent Space blocks converts badly, because those are exactly the blocks the Workflow palette does not carry — they are the reason pipelines exist. Press Compile and drag the pipeline into the workflow as a single block instead: every block inside it stays intact. The report says so when it applies.

Saving, renaming, reopening

  • Saving a workflow writes a .workflow file and co-saves every pipeline it references as a .pipeline file 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 .pipeline files 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_scope20 subjects × 2 sessions produces
Per subject, per session40 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 session2 runs — one per session, across all subjects
Combined subjects, combined sessions1 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

A run under Per subject, combined sessions only ever sees one subject at a time, so leave-one-subject-out has nothing to hold out. Set 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

BCILattice Trainer carries the same four options under 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

Wire two or more models into a Model Collection and its 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_sourcePlan
FilteredFree — the band-passed recording, the one pre-analysis entry
ICA Cleaned DataResearcher
Quality Checked DataResearcher
EpochsResearcher
Normalized DataResearcher
Channel Selected DataResearcher
Dim Reduced DataResearcher
FeaturesResearcher
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

Every gated stage has a block that produces the same thing, grouped under Analysis (BCILattice) in the palette. Start from 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 DataChannel Selection
Signal Projection (images)Signal Projection
Time-Frequency (ERSP)Time-Frequency Features
Dim Reduced DataPCA · FastICA · KernelPCA · TruncatedSVD
FeaturesSelectKBest · 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.

1

Pick a provider and model

Claude or OpenAI, and one of their models. Build with AI runs on BCINexus AI, so there is no account or API key to connect: it needs the AI add-on, and each request is paid from your workspace's monthly AI credit.
2

Describe the pipeline

Be concrete about signal type, processing and goal. “Bandpass EEG 8–30 Hz, select the best channels, train a classifier for left vs right imagery” gives a far better skeleton than “make me a BCI”.
3

Review before you trust it

The result loads into a new tab as a draft. Check every block is right for your case, set the parameters (frequencies, window sizes, model settings), and fix any ports it left unconnected.

It produces a draft, not a finished pipeline

The model places blocks and guesses connections. It does not know your sampling rate, your montage or your paradigm, and it does not validate the graph — Compile does. Treat the output as a starting skeleton you review, exactly as the dialog warns.

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

MessageWhat to do
Compilation failed: no input nodeAdd an Input Block (Pipeline) or a Select Files block (Workflow).
Exactly one final output node requiredYou have zero or two terminal nodes. Data Graph does not count — Output Block, Graph Output and Graph Endpoint do.
No model or trainer nodeA pipeline has to actually train or apply something. Add a model block or a BCILattice Trainer.
Compiled pipeline not foundThe pipeline was never compiled, or was compiled in a different study. Open its Pipeline tab and press Compile.
Pipeline file not found on loadThe .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 groupsCross-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 runsSelect 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 drawRead the Graph Error port on Data Graph / Graph Output — it carries the exact reason.
This workflow has no Select Files blockShown 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 allShown 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 inputTwo 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.

Using the ML Suite v1.0 · BCINexus Platform · 2026-05-20