LQL tutorial
A link connects workflow nodes: it reads inputs and outputs of some steps and writes to others. The Link Query Language (LQL) is how a link names those inputs and outputs.
This page covers the queries most links need. Fan-out over repeated steps, reference selectors, tags and the advanced examples are in LQL advanced. Every selector, flag and rule is listed in the LQL reference.
Two concepts: ids and positions
Workflows range from a fixed list of steps to dynamic ones where users add, remove and reorder steps. LQL therefore combines two concepts:
- Ids are the names of steps and nested workflows. When every step has a unique id, a path of ids is all a link needs.
- Position selectors relate a step to the steps before or after it. In a dynamic workflow several steps may share an id, and a selector picks the first, the last, all of them, or the one right after a given step.
A path of bare ids is the common case. Selectors appear only where the workflow shape makes an id ambiguous.
Query structure
Every query has the form:
NAME(FLAGS)?:PATH
NAME is the alias the handler uses to read or write the matched value.
FLAGS is an optional comma-separated list inside parentheses; the flags
are optional, call and template. PATH is a /-separated list of
segments resolved relative to the workflow that declares the link. A
leading / is not allowed.
A query that targets a script input:
value:myworkflow/subworkflow/script/input1
myworkflow, subworkflow and script are ids. A bare id means "the
first node with this id", so the query above is the same as:
value:first(myworkflow)/first(subworkflow)/first(script)/input1
A query with no path targets the workflow that declares the link. The
forms NAME, NAME:, NAME:. and NAME:./ are equivalent, and
NAME:./path is accepted. A . in the middle of a path is not.
Simple cases
Every case below lives in this one workflow. load produces a table,
clean filters it and reports statistics, fit fits a model, and
summary reports. Between them, reviews is a nested dynamic workflow
with a single step type: users add as many review steps as they want,
or remove them all.
{
id: 'demo',
type: 'static',
steps: [
{id: 'load', nqName: 'Pkg:LoadTable'}, // out: table
{id: 'clean', nqName: 'Pkg:CleanTable'}, // in: table, threshold; out: table, stats
{id: 'fit', nqName: 'Pkg:FitModel'}, // in: table, stats, iterations; out: model
{id: 'reviews', type: 'dynamic',
stepTypes: [{id: 'review', nqName: 'Pkg:Review'}], // in: model, notes; out: score
initialSteps: ['review']},
{id: 'summary', nqName: 'Pkg:Summary'}, // in: model, scores, notes
],
links: [/* the cases below */],
}
Connect two steps
{id: 'toClean', from: 'table:load/table', to: 'input:clean/table'}
The alias before the colon (table, input) is what the handler works
with. The path after it names the step and its io. In a static workflow
every id is unique, so a path of ids is all the link needs. Without a
handler the default handler copies each
from value to the to entry at the same position.
Reach into a nested workflow
{id: 'toReview', from: 'model:fit/model', to: 'target:reviews/review/model'}
Paths are relative to the workflow that declares the link, and cross
into nested workflows segment by segment. Inside reviews the id
review is not unique, and a bare id means the first match, so this
link feeds the first review only. The same link declared inside
reviews would write target:review/model.
Read several values in one handler
{
id: 'iterations',
type: 'validator',
from: ['threshold:clean/threshold', 'n:fit/iterations'],
to: 'target:fit/iterations',
handler: ({controller}) => {
if (controller.getFirst('threshold') > 0.5 && controller.getFirst('n') < 100)
controller.setValidation('target', {warnings: ['Few iterations for a strict threshold']});
else
controller.setValidation('target', undefined);
},
}
Aliases are distinct within a link, across from and to alike. A
validator reads its inputs and writes a result to the io named in to.
Several inputs and outputs of one step
When a link moves several ios between the same two steps, the
template flag expands a |-list in the last segment into one query
per io. The alias becomes the prefix plus the io name.
{
id: 'toFit',
from: 'in_(template):clean/table|stats',
to: 'out_(template):fit/table|stats',
}
This is the same as writing in_table, in_stats, out_table and
out_stats by hand. Use _ as the prefix to get aliases equal to the
io names. The list may only appear in the last segment, never in the
path to the step.
The same paths in checks and rules
Checks and rules use the same paths. A check names one io and has no alias:
{id: 'iterationsRange', type: 'check', io: 'fit/iterations', check: {min: 1, max: 10000}}
A rule reads and writes through aliases like any link. Rule queries take
no flags but optional and template:
{
id: 'strict',
type: 'rule',
from: 'threshold:clean/threshold',
to: 'n:fit/iterations',
when: 'gt(threshold, 0.5)',
effects: ['warning(n, "Consider more iterations for a strict threshold")'],
}
A step that may be absent
A link whose query matches nothing is not created. The optional flag
creates it anyway. Reading an alias that matched nothing throws, so the
handler checks getMatchedInputs first:
{
id: 'carryNotes',
from: 'notes(optional):reviews/review/notes',
to: 'target:summary/notes',
handler: ({controller}) => controller.setAll('target',
controller.getMatchedInputs().has('notes') ? controller.getFirst('notes') : ''),
}
Here review means the first review. When the user removes every review
the link still runs and writes an empty string.
Every step with one id
all matches every direct child with the id, so one query yields many
values. The handler reads them with getAll:
{
id: 'collectScores',
from: 'scores:reviews/all(review)/score',
to: 'target:summary/scores',
handler: ({controller}) => controller.setAll('target', controller.getAll('scores')),
}
This is one link that sees all reviews at once. When each review should
get a link of its own, the advanced page's expand is the tool.
The host workflow
A pipeline validator targets a workflow node. A query with no path is the workflow that declares the link:
{
id: 'needsReviews',
type: 'pipelineValidator',
from: 'scores:reviews/all(review)/score',
to: 'self',
handler: ({controller}) => {
const scores = controller.getAll('scores');
controller.setValidation('self', scores.length ? undefined : {errors: ['Add at least one review']});
},
}
What the advanced page adds
LQL advanced covers a base query
with expand, which turns one link definition into one link per
matching step, and the reference selectors same, before and after
that anchor from and to to each of those steps. It also covers tags,
which match across nesting levels, and the wildcard io selectors, with
one example per topic on a single workflow. Form logic built from rules,
such as a computed default and a lookup table, is in
Rules and checks.