Skip to main content

Configuration and links

The tutorial builds a static workflow whose steps pass data with plain data links. This page introduces the rest of the configuration and the other kinds of links on a complete workflow: the Bioreactorworkflowdemo package simulates daily bacterial growth in a fed-batch bioreactor. Every snippet below is an excerpt of its src/package.ts.

To try it, run the BioreactorWorkflow model from ModelHub in Apps > Compute. Each section ends with a pointer to the reference that covers the topic in full.

The configuration tree​

A configuration is a tree. The provider returns the root workflow, a workflow holds steps, and a step is a script or a nested workflow. Every node has an id, unique among its siblings, and links address nodes by these ids.

A workflow has one of two types:

  • A static workflow lists its steps in steps. The user can't add or remove them. The tutorial's workflow is static.
  • A dynamic workflow lists the kinds of steps it can hold in stepTypes, and the steps it starts with in initialSteps. The user adds, removes and reorders steps while working.

The bioreactor workflow is static, and nests a dynamic workflow of cultivation days between the configuration and the summary:

{
id: 'bioreactorWorkflow',
nqName: 'BioreactorWorkflowDemo:BioreactorWorkflow',
version: '1.0',
type: 'static',
steps: [
{id: 'bioreactorConfiguration', nqName: 'BioreactorWorkflowDemo:BioreactorConfiguration'},
{
id: 'dailyCultivation',
type: 'dynamic',
stepTypes: [{id: 'dayCalculation', nqName: 'BioreactorWorkflowDemo:DayCalculation'}],
initialSteps: [{id: 'dayCalculation'}],
},
{id: 'summary', nqName: 'BioreactorWorkflowDemo:Summary'},
],
}

The workflow starts with one day. Add days from the step tree, reorder them, or remove one. All days share the id dayCalculation, so a link that feeds them needs a way to say "the first day", "every day" or "the day after this one". The sections below use the first two, and Node meta links the third.

See Workflow types for every field.

A link reads values through its from queries, writes through its to queries, and runs whenever a value it reads changes. A query has the form alias:step/io: the path names a step and one of its inputs or outputs, and the alias is the name the link uses for that value.

The type of a link decides what it writes:

TypeWritesTypical use
data (the default)Input valuesPass a result to the next step, compute an input from others
validatorMessages on an inputErrors that block the run, warnings and notifications that don't
pipelineValidatorMessages on a workflowCheck the workflow as a whole, such as a missing step
nodemetaTitles and descriptions of stepsDay numbers in the step tree
metaDisplay settings of an inputHide an input, replace dropdown items
checkMessages on an input, or hides itValidation without code, see Rules and checks
ruleMessages, display settings and valuesForm logic without code, see Rules and checks

Actions are separate: they run when the user clicks them, not when a value changes. See Actions.

The configuration step outputs the initial reactor state and the growth kinetics. Two links pass them on:

{
id: 'initial-volume',
from: 'source:bioreactorConfiguration/volume',
to: 'target:dailyCultivation/first(dayCalculation)/incomingVolume',
defaultRestrictions: {target: 'disabled'},
},
{
id: 'maximum-growth-rate',
from: 'source:bioreactorConfiguration/maximumGrowthRate',
to: 'target:dailyCultivation/all(dayCalculation)/maximumGrowthRate',
defaultRestrictions: {target: 'disabled'},
},
  • first(dayCalculation) matches the first day only. The initial state feeds day 1, and every later day starts from the day before it.
  • all(dayCalculation) matches every day, including days the user adds later. The kinetics are the same for the whole cultivation.
  • Neither link has a handler, so the default handler copies each from value to the to entry at the same position. A link without type is a data link.

A handler computes what to write. The summary link collects every day into one table:

function populateDailySummary({controller}) {
const summary = makeDailySummary((field) => controller.getAll(`day_${field}`) ?? []);
controller.setAll('summary', summary, 'restricted');
}

{
id: 'daily-summary',
from: 'day_(template):dailyCultivation/all(dayCalculation)/ solutionAdded | substrateAdded | finalBiomass',
to: 'summary:summary/summaryInput',
handler: populateDailySummary,
}
  • day_(template) gives one alias per listed io, day_solutionAdded, day_substrateAdded and so on. The package lists every daily input and output; the excerpt keeps three.
  • A handler reads and writes only through the controller, by alias. getFirst reads the first matched value, getAll all of them, here one per day in step order, and setAll writes to every matched input. makeDailySummary is a plain function of the package that builds a dataframe with one column per field.
  • The writes apply when the handler finishes. A handler can be async, and a newer run of the same link cancels an unfinished one.

See Data links and Common controller methods for every method, and Link queries for first, all and (template).

Consistency​

A value a data link writes is tracked: the input remembers the value the link set. Two things follow:

  • When the user edits a linked input, the input is marked inconsistent, and the user can reset it to the link's value.
  • While a step's results are current, a link doesn't overwrite its inputs. A new upstream value marks the step inconsistent instead, and Update applies the new values and reruns it.

The restriction of the write sets how strict this is:

RestrictionThe user can edit the inputAn edit is marked inconsistent
restricted (the default)YesYes
disabledNoNot applicable
infoYesNo, the link value is still tracked
noneYesNo, nothing is tracked

A handler passes the restriction as the third argument of setAll. A link with the default handler sets it per alias with defaultRestrictions. Every link into the days uses disabled: a day's incoming state is what the reactor holds, and the kinetics belong to the organism, so the user edits them on the configuration step only. The only inputs left editable on a day are the daily feeds, solutionAdded and substrateAdded.

Try it: run the configuration and the first day, change the maximum growth rate on the configuration step and rerun it. The day is marked inconsistent, and Update reruns it with the new rate.

See Consistency.

Validators​

A validator link adds messages to an input. An error blocks the run of the step, and warnings and notifications don't. The reactor must have room for the liquid it starts with:

{
id: 'volume-limit',
type: 'validator',
from: ['initial:bioreactorConfiguration/initialVolume', 'max:bioreactorConfiguration/maxVolume'],
to: 'target:bioreactorConfiguration/maxVolume',
handler: ({controller}) => {
const initial = controller.getFirst<number>('initial')!;
const max = controller.getFirst<number>('max')!;
controller.setValidation('target',
max <= initial ? {errors: ['The maximum volume must exceed the initial volume']} : undefined);
},
}

setValidation with undefined removes the link's messages. For an expensive validator, debounce on the link waits that many milliseconds after the last change before it runs.

The same error needs no code as an expression check:

{
id: 'volume-limit',
type: 'check',
io: 'bioreactorConfiguration/maxVolume',
check: {validator: 'value > initialVolume'},
vars: {initialVolume: 'bioreactorConfiguration/initialVolume'},
message: 'The maximum volume must exceed the initial volume',
}

See Rules and checks for checks and rules, and Validators for messages with actions.

Pipeline validators​

A pipeline validator puts messages on a workflow instead of an input, and also runs again when steps are added, removed or moved. A cultivation without days has nothing to simulate:

{
id: 'days-required',
type: 'pipelineValidator',
from: [],
to: 'days',
handler: ({controller}) => {
const outline = controller.getOutline();
const dayCount = 'steps' in outline ? outline.steps.length : 0;
controller.setValidation('days',
dayCount === 0 ? {errors: ['Add at least one cultivation day']} : undefined);
},
}
  • The link is defined in the links of dailyCultivation. A to query without a path, days, targets the workflow the link is defined on.
  • It reads no values, from: [], so only changes of the steps run it. getOutline returns the steps of that workflow.

A pipeline validator can also compare values across steps. Every feed adds liquid, and a feeding plan that overfills the reactor gets a warning before anything runs:

{
id: 'volume-plan',
type: 'pipelineValidator',
from: [
'initial:bioreactorConfiguration/initialVolume',
'max:bioreactorConfiguration/maxVolume',
'added:dailyCultivation/all(dayCalculation)/solutionAdded',
],
to: 'days:dailyCultivation',
handler: ({controller}) => {
const max = controller.getFirst<number>('max')!;
const added = controller.getAll<number>('added') ?? [];
let volume = controller.getFirst<number>('initial')!;
const day = added.findIndex((v) => (volume += v) > max);
controller.setValidation('days', day < 0 ? undefined :
{warnings: [`Day ${day + 1}: volume reaches ${volume.toFixed(1)} L, above the ${max} L limit`]});
},
}

Try it: with an initial volume of 1 L and a limit of 5 L, add three days and set the solution added to 1, 2 and 2 L. The workflow warns that day 3 reaches 6.0 L. Reorder or remove a day and the message follows.

See Pipeline validators.

A node meta link writes the title or description of a step instead of a value. Each day's title shows its number, and its final biomass once the day has run, such as Day 2 - Biomass 3.4 g/L:

{
id: 'day-title',
type: 'nodemeta',
base: 'base:expand(dayCalculation)',
from: 'biomass:same(@base, dayCalculation)/finalBiomass',
to: 'title:same(@base, dayCalculation)/title',
handler: ({controller}) => {
const day = controller.getBasePosition()!.position + 1;
const biomass = controller.getFirst<number>('biomass');
controller.setDescriptionItem('title',
biomass == null ? `Day ${day}` : `Day ${day} - Biomass ${biomass.toFixed(1)} g/L`);
},
}
  • base: 'base:expand(dayCalculation)' makes one copy of the link per day, and same(@base, dayCalculation) is that day. The copies are made again when days are added, removed or moved, so the numbers follow the order.
  • getBasePosition returns the position of the day among the steps of dailyCultivation.
  • from reads an output, so the title changes when the day runs.

The same pattern carries the reactor state from one day to the next. after+(@base, dayCalculation) is the day right after the base day, and the last day has none, so its copy of the link writes nothing:

{
id: 'chain-biomass',
base: 'base:expand(dayCalculation)',
from: 'previous:same(@base, dayCalculation)/finalBiomass',
to: 'next:after+(@base, dayCalculation)/incomingBiomass',
defaultRestrictions: {next: 'disabled'},
}

See Node meta links for the descriptions a link can write, and Advanced link queries for expand and the position functions.

Where to go next​