Workflows over HTTP
Run several tools wired together in one request: a product handed downstream, a step that runs only if a condition holds, and a step that runs once per item of a list.
| Run | POST /api/workflows/run |
| Check without running | POST /api/workflows/validate |
| Ready-made pipelines | GET /api/workflows/templates, and ?id=<template> for one graph |
| Interactive reference | /api/openapi |
What it does
The HTTP API runs one tool per request. A workflow is a graph of tool calls, the same graph the workflow builder in the editor draws, and it adds the three things a single call cannot say:
- Wires. One step's output becomes another step's input. Wired into
$subject, a step's product becomes the molecule the next step works on, so a digest runs on the amplicon rather than on the template. - Conditions. A step carrying
$whenExpressionruns only when the expression is true. When it is false the step is reported asunmet, which is an answer, not a failure. - For-each. A step carrying
scatter: ["<input>"]runs once per element of that input, and every output comes back as a list with one entry per element.
Every step is a tool from the tool listing, with exactly the parameters, bounds and defaults that listing documents. There is no second set of workflow-only tools.
The request
{
"graph": {
"name": "a label for the run record",
"nodes": [{ "id": "pcr", "tool": "pcr_amplify", "params": { "forward": "…", "reverse": "…" } }],
"edges": [{ "id": "w1", "from": { "node": "pcr", "port": "record" }, "to": { "node": "cuts", "port": "$subject" } }]
},
"_subject": { "sequence": "…", "name": "…", "circular": true }
}A node is { id, tool, params }. A wire is { id, from: { node, port }, to: { node, port } }: the
from port is an output name from the tool listing, the to port an input name or $subject.
_subject at the top level of the body (a wire still names the port $subject) is the molecule every step works on unless a wire hands it a different
one, in the same shape the single-tool endpoints take.
A worked example: amplify an insert, then characterise the product
The template is the bundled pUC57-LoxP-GSK3-LoxP plasmid, 8,767 bp, circular. We amplify between
the two universal sequencing primers, M13 forward (−20) GTAAAACGACGGCCAGT and M13 reverse
CAGGAAACAGCTATGAC, then count four enzymes' sites on the product and size it on a gel. Both
downstream steps are wired to the PCR step's record output, so they see the amplicon, not the
plasmid.
curl -s https://<host>/api/workflows/run \
-H 'content-type: application/json' \
-d '{
"graph": {
"nodes": [
{ "id": "pcr", "tool": "pcr_amplify",
"params": { "forward": "GTAAAACGACGGCCAGT", "reverse": "CAGGAAACAGCTATGAC" } },
{ "id": "counts", "tool": "enzyme_cut_counts",
"params": { "enzymes": ["EcoRI", "BamHI", "HindIII", "XhoI"] } },
{ "id": "gel", "tool": "run_gel_lane", "params": { "source": "plasmid" } }
],
"edges": [
{ "id": "w1", "from": { "node": "pcr", "port": "record" }, "to": { "node": "counts", "port": "$subject" } },
{ "id": "w2", "from": { "node": "pcr", "port": "record" }, "to": { "node": "gel", "port": "$subject" } }
]
},
"_subject": { "name": "pUC57-LoxP-GSK3-LoxP", "circular": true, "sequence": "…8,767 bases…" }
}'The run record answers step by step, in the order the steps ran:
| Step | Status | What it returned |
|---|---|---|
pcr | ok | one product, 6,184 bp, annealing temperature 58.1 °C (forward primer 57.9 °C, reverse 52.1 °C, SantaLucia & Hicks 2004 unified table), no primer-dimer risk |
counts | ok | on the product: HindIII 1, XhoI 2, EcoRI 0, BamHI 0 |
gel | ok | one band, 6,184 bp, linear |
The product length is the arithmetic you can check by hand: the forward primer starts at base 379 and the reverse primer's site ends at base 6,562 of the plasmid, which is 6,184 bases inclusive. The site counts are the plasmid's own sites that fall inside that span (HindIII at 5,627, XhoI at 2,576 and 2,675). The gel lane reads the product as a linear molecule because it arrived as one: had the lane been run on the plasmid itself it would show supercoiled and nicked forms instead.
The same graph, with a condition
Add "$whenExpression": "subject.circular" to a step's params and it runs only on a circular
molecule. Sent a map step that way, with the plasmid as _subject:
_subject.circular | map status | Why |
|---|---|---|
true | ok | the condition held, so the map was drawn |
false | unmet | not run: this step's condition was false |
The run as a whole is still ok: true in both cases. A condition exists to leave a branch unrun, so
an unrun branch is not an error.
A condition can read the subject (subject.length, subject.circular) and the step's own inputs,
with == != < > <= >= and or not + - * / % ?? and the functions abs, ceil, contains, floor, isset, len, lower, max, min, number, round, upper. There is no method call and no ternary: a condition
returns one verdict. The interactive reference at /api/openapi prints the same list from the same
declaration.
The same check over three primers, with for-each
scatter runs one step once per element. Checking where three primers anneal on the plasmid is one
step rather than three requests:
{ "id": "check", "tool": "primer_check",
"params": { "primer": ["GTAAAACGACGGCCAGT", "CAGGAAACAGCTATGAC", "TAATACGACTCACTATAGGG"] },
"scatter": ["primer"] }The step reports three jobs, and each output is a list in the order the primers were given:
siteCount is [1, 1, 0]. M13 forward anneals once on the top strand and M13 reverse once on the
bottom strand, both as exact matches; the T7 promoter primer has no site on this plasmid. Each job's
own input, result and status is kept under jobs, so "two of three bound" is visible without
re-running anything.
Checking a graph before running it
POST /api/workflows/validate takes the same body and runs nothing. It answers ok: true with no
issues for a graph that will run, or ok: false with each blocking problem naming its step or wire:
an unknown tool, a wire into a port that does not exist, a cycle. /run performs the same check first
and refuses a graph that fails it with 400 and the same issue list, so a pipeline is never half
run because step four was misspelled.
What the status codes mean
- 200 — the run happened. Read
nodes: each step isok,error(the tool declined, with its reason),skipped(a step it depends on did not produce anything) orunmet(its condition was false). A step failing does not fail the request, because "step three of six declined, and here is why" is the answer a caller needs and a status code cannot carry it. - 400 — the request itself is unusable: no
graph, a_subjectthat cannot be read, or a graph that cannot run. The body says which.
Starting from a template
GET /api/workflows/templates lists the ready-made pipelines with what each does and which tools it
chains (restriction map, diagnostic digest, colony PCR screen, align and build a tree, batch Sanger
verification and others). ?id=starter_amplify_characterize returns one graph, which can be posted
straight back to /run once its open parameters (the primers, the enzymes) are filled in.
Limits worth knowing
- A run is one request, with a budget. There is no job to poll. A graph holds at most 100 steps,
and a run may compute for 120 seconds: past that, the steps that finished keep their results, the
rest are marked
cancelled, andstoppedBysays why. The validation step is what stops a graph that cannot run from starting at all. - Templates that read the project (the list of traces in a project, a named document) need an
editor session. Over HTTP every step works on
_subjector on what is wired into it.