App API
This page documents HTTP endpoints exposed by an installed SeqDesk instance. They are the endpoints the SeqDesk interface itself calls, which makes them stable enough to script against — but it also means they are shaped for the interface, not designed as a public REST product. Read the response shapes here rather than assuming a convention.
For the public seqdesk.org endpoints (pipeline catalog, MIxS registry, release feed) see the Pipeline Registry API instead. Nothing on this page is served by seqdesk.org, and nothing on that page can see your data.
The auth model in one paragraph
There is no API-key mechanism and no middleware. Every route calls
getServerSession() itself and makes its own decision, so authentication means
sending a valid NextAuth session cookie. Four patterns recur:
| Pattern | Typical response when it fails |
|---|---|
| Any authenticated user | 401 {"error":"Unauthorized"} |
| Facility admin only | 403 {"error":"Unauthorized"} — note the status, not 401 |
| Facility admin, and not a demo session | 403 with a specific message, e.g. "Pipeline execution is disabled in the public demo." |
| Shared secret, no session | the weblog endpoint only |
Because each route decides independently, the same failure can be a 401 on one
endpoint and a 403 on another. The per-endpoint sections below state which.
A handful of endpoints are deliberately unauthenticated because they run before
anyone can log in or because they contain no facility data:
GET /api/version, GET /api/setup/status, GET /api/mixs-checklists, and
GET /api/sequencing-tech. Everything else requires a session — including
GET /api/modules, which reports which modules an instance has enabled and is
therefore not something to expose to an anonymous caller.
Instance metadata
GET /api/version
The installed version, read from the release’s package.json. No session
required. Useful as a liveness probe and for asserting which release a host is
actually running after an update.
curl -s https://your-host/api/version{ "version": "1.1.125" }Do not confuse this with GET /api/version on seqdesk.org, which returns the
latest published release and has an entirely different shape.
GET /api/setup/status
The install-status probe the setup and login pages poll before anyone can sign in. Unauthenticated by design, and deliberately narrow: it reports database reachability and bootstrap progress, never account addresses.
Top-level keys: exists, configured, phase, steps, nextAction,
database, install, plus bootstrapAccounts when a seed pass ran and error
when something failed. The response is served Cache-Control: no-store.
GET /api/modules
Which optional modules this instance has enabled. Requires a session — any
signed-in role, because the forms every user sees are module-driven. Without a
session cookie it answers 401 {"error":"Unauthorized"}.
{
"modules": {
"ai-validation": true, "mixs-metadata": true, "account-validation": false,
"funding-info": false, "billing-info": false, "ena-sample-fields": true,
"dynamic-studies": false, "notifications": false
},
"globalDisabled": false
}modules is the stored configuration merged over the built-in defaults, so every
module id that has a default is always present, whatever the instance saved. An
instance that has never saved a module configuration gets exactly the defaults
shown above. Changing the configuration is a separate admin-only endpoint; see
Administration → Modules.
Form schemas
The dynamic form configuration is exposed read-only so external tooling can render the same field set the SeqDesk interface renders.
GET /api/form-schema
Returns the sequencing-order form schema as configured in the
Order Form Builder. Requires a session
(401 otherwise).
{
"fields": [
{ "id": "field_library_strategy", "name": "library_strategy", "type": "select",
"options": ["WGS", "AMPLICON"], "required": true, "groupId": "group_sequencing",
"visible": true, "perSample": false }
],
"groups": [
{ "id": "group_sequencing", "name": "Sequencing Information", "icon": "Dna", "order": 1 }
],
"version": 3,
"enabledMixsChecklists": ["ERC000022"],
"perSampleFields": [
{ "id": "field_barcode", "name": "barcode", "type": "barcode", "perSample": true, "visible": true }
]
}Three things about this response are easy to get wrong:
versionis an integer —OrderFormConfig.version, which starts at1and is bumped on every save. It is not a timestamp. When no form has been saved yet, the endpoint returns the built-in default schema withversion: 1.perSampleFieldscontains whole field objects, not names. It is the subset offieldswhereperSample && visible, precomputed so callers do not have to re-derive it.- The field list is filtered twice: by enabled modules, then by role. Fields
marked
adminOnlyare omitted entirely for researchers, andenabledMixsChecklistscomes back as[]unless themixs-metadatamodule is on.
GET /api/study-form-schema
The study metadata form. Requires a session.
GET /api/study-form-schema
GET /api/study-form-schema?studyId=<id>Returns { fields, studyFields, perSampleFields, groups, modules } — note that
unlike the order schema there is no version and no
enabledMixsChecklists key. studyFields and perSampleFields split the field
list into study-level and sample-level; modules reports which modules’ fields
are currently contributing.
?studyId= matters only when the dynamic-studies module is enabled: it
resolves that study’s own StudyFormConfig row instead of the global study form.
MIxS checklists
MIxS is the Genomic Standards Consortium’s Minimum Information about a Sequence checklist family. SeqDesk ships checklist definitions, admins enable specific checklists in the form builder, and researchers then see the corresponding required fields.
GET /api/mixs-checklists
Lists the active checklists. Unauthenticated.
curl -s https://your-host/api/mixs-checklists{
"checklists": [
{
"name": "soil",
"accession": "ERC000022",
"description": "GSC MIxS soil environmental package",
"fieldCount": 53,
"mandatoryCount": 12,
"deprecated": false
}
],
"total": 16,
"version": 4
}total is the number of returned checklists, and version is the MIxS registry
version currently active on this instance — the same integer a study pins in
Study.mixsVersion.
Single-checklist lookup returns the full definition (every field, its data type, unit options, MIxS section, and whether it is mandatory), not the summary above:
curl -s "https://your-host/api/mixs-checklists?accession=ERC000022"
curl -s "https://your-host/api/mixs-checklists?name=soil"
curl -s "https://your-host/api/mixs-checklists?accession=ERC000022&version=3"?version= resolves the definition as it existed at that registry version, which
is how a study pinned to an older MIxS release keeps rendering the fields it was
authored with. An unknown accession or name returns
404 {"error":"Checklist not found: ERC000022"}.
GET /api/mixs-templates
The same definitions in template form, ready to be embedded in the form builder.
Requires a session (401 otherwise). ?name=<checklist> narrows to one
template and falls back to fuzzy matching — normalized name equality, then
substring matching — before giving up with 404 {"error":"Template not found"}.
When several templates match, the one with the most fields wins. ?version=
behaves as above.
Sequencing uploads
The sequencing tab uses a three-call chunked protocol when reads or artifacts are attached to a sample. All three calls are facility-admin only and blocked in the public demo. Mimic the same flow when bulk-loading data.
The order must be SUBMITTED or COMPLETED — otherwise you get
400 {"error":"Sequencing data can only be managed on submitted or completed sequencing orders"}.
Initiate
POST /api/orders/[id]/sequencing/uploads
{
"targetKind": "read",
"targetRole": "R1",
"originalName": "S1_R1.fastq.gz",
"expectedSize": 1843921023,
"checksumProvided": "6f1b9c0d4e2a...",
"mimeType": "application/gzip",
"sampleId": "cm2x7q9r80001abcd",
"metadata": { "dataClass": "raw", "sequencingRunId": "cm2x7qa1z0002abcd" }
}targetKind, targetRole, originalName, and expectedSize are required; a
missing one returns
400 {"error":"targetKind, targetRole, originalName, and expectedSize are required"}.
For targetKind: "read", targetRole must be R1 or R2 and the filename must
carry one of the configured sequencing-file extensions
(.fastq.gz, .fq.gz, .fastq, .fq by default) — otherwise
"Read uploads must use an allowed sequencing file extension".
{
"success": true,
"uploadId": "cm2x7qb4k0003abcd",
"tempPath": "orders/<orderId>/.tmp/cm2x7qb4k0003abcd/S1_R1.fastq.gz",
"status": "PENDING",
"receivedSize": 0
}Send chunks
PATCH /api/orders/[id]/sequencing/uploads/[uploadId]
The chunk is the raw request body, and the byte offset goes in the
x-seqdesk-offset header. There is no multipart wrapper and no JSON envelope.
curl -X PATCH "https://your-host/api/orders/$ORDER/sequencing/uploads/$UPLOAD" \
-H "x-seqdesk-offset: 0" \
--data-binary @chunk-0000 \
--cookie "$COOKIE_JAR"{ "success": true, "uploadId": "cm2x7qb4k0003abcd", "receivedSize": 8388608,
"expectedSize": 1843921023, "status": "UPLOADING" }The offset must equal the server’s current receivedSize exactly. Anything else
is rejected with
400 {"error":"Upload offset does not match current upload size"} — which is
also how you resume: read receivedSize back and continue from there. Status
flips from UPLOADING to READY automatically once receivedSize reaches
expectedSize.
Complete
POST /api/orders/[id]/sequencing/uploads/[uploadId]/complete
No body. The server promotes the file out of .tmp into its final location and,
for a read upload, creates or updates the sample’s Read record.
{ "success": true, "uploadId": "cm2x7qb4k0003abcd",
"finalPath": "orders/<orderId>/reads/S1/...", "size": 1843921023, "status": "COMPLETED" }An incomplete upload fails with 400 {"error":"Upload is incomplete"}; a read
upload with no sampleId fails with
400 {"error":"Read uploads require a target sample"}.
To abandon a session, DELETE /api/orders/[id]/sequencing/uploads/[uploadId]
removes the partial temp file and marks the row CANCELLED.
checksumProvided is recorded, not verified. The value you supply at
initiate is stored on the resulting Read or SequencingArtifact as its
checksum; nothing recomputes the MD5 during complete. If you want a verified
checksum, run POST /api/files/checksum afterwards —
it hashes the file on disk and overwrites the stored value.
File operations
These endpoints back the in-app file browser and downloader. They respect demo-session restrictions (Demo mode) and the delivery-publication rules.
GET /api/files/download?path=<rel>
Streams a file as application/octet-stream with a Content-Disposition
attachment header. When the path resolves to a Read, the response also carries
X-SeqDesk-Read-Data-Class so a client can tell raw from cleaned data without a
second request.
Requires a session. Demo sessions get
403 {"error":"Downloads are disabled in the public demo."}.
The path is relative to the configured data base path — absolute paths and
.. traversal are rejected with 400 {"error":"Invalid file path"}, and an
unconfigured data base path is 400 {"error":"Data base path not configured"}.
Authorization is per-file, not per-role-only. A facility admin may download anything with an allowed extension. A researcher must satisfy one of:
| File is | Researcher may download when |
|---|---|
a Read | they own the order, the delivery has been published (sequencingFilesPublishedAt is set), and the read’s dataClass is cleaned |
a SequencingArtifact | they own the order, the delivery has been published, and visibility is customer |
an Assembly | allowUserAssemblyDownload is enabled, they own the order or the study, and the order is COMPLETED |
Assemblies are additionally gated by extension
(.fa, .fasta, .fna, and their .gz variants). A researcher hitting an
assembly while the setting is off gets
403 {"error":"Assembly downloads are disabled by the facility administrator."};
any other failure is 403 {"error":"Access denied"}.
GET /api/files/preview?path=<path>
Serves a file inline for in-browser viewing — HTML QC reports are the main use.
Allowed extensions are html, htm, pdf, txt, tsv, csv, log, json,
and the file must be at most 100 MB. Access is validated against the pipeline run
the file belongs to, using the same rule as the run endpoints below. In demo
mode the endpoint serves generated stand-in reports instead of reading disk.
Unlike download, preview also accepts an absolute path (still checked
against traversal), because pipeline artifacts are recorded with absolute run
paths.
POST /api/files/checksum
Facility-admin only (403 {"error":"Only facility admins can calculate checksums"}).
Computes MD5 for up to 50 paths per call and writes each result onto the
matching Read record. Checksums are raw lowercase hex with no md5: prefix.
// Request
{ "filePaths": ["orders/abc/S1_R1.fastq.gz", "orders/abc/S1_R2.fastq.gz"] }// Response
{
"success": true,
"results": [
{ "filePath": "orders/abc/S1_R1.fastq.gz", "checksum": "6f1b...", "updatedReadRecord": true },
{ "filePath": "orders/abc/S1_R2.fastq.gz", "checksum": "a3d9...", "updatedReadRecord": false,
"warning": "No assigned read record found; checksum was not stored in database" }
],
"summary": { "total": 2, "successful": 2, "failed": 0, "updatedReadRecords": 1, "notLinkedToRead": 1 }
}Per-file failures do not fail the request: a path outside the data base path, or
a missing file, comes back as a results entry with an error field while the
rest are still hashed. More than 50 paths is a hard
400 {"error":"Maximum 50 files at a time"}.
POST /api/files/delete
Facility-admin only (401 here, not 403). Deletes each path from disk and
repairs the referencing Read records: if the deleted file was the read’s only
file the row is deleted, otherwise the corresponding file1/file2 column is
cleared and the row survives. Already-deleted entries are reported per path in
errors rather than aborting the batch.
Pipeline runs
A run is created first and launched second, which is why there are two calls.
Both go through /api/pipelines/runs.
GET /api/pipelines/runs
Lists runs. Requires a session. Facility admins see everything; researchers see
only runs whose target they own and which have been selected as the final
result for that target. Query parameters: pipelineId, status, studyId,
orderId, limit (default 50), offset (default 0), and
publishedOnly=true (aliases: userVisible=true, visible=user), which applies
the researcher-visibility filter to an admin listing too.
Returns { "runs": [ … ], "total": <n>, "limit": <n>, "offset": <n> }. Each run
is enriched with its resolved result files, so the listing is heavier than a raw
PipelineRun row.
POST /api/pipelines/runs
Creates a pending run. Facility-admin only (403). It validates aggressively
before writing anything, so most integration mistakes surface here rather than at
launch.
{
"pipelineId": "fastqc",
"orderId": "cm2x7q8lp0000abcd",
"sampleIds": ["cm2x7q9r80001abcd", "cm2x7q9r80002abcd"],
"config": {},
"executionMode": "local"
}{
"success": true,
"run": {
"id": "cm2x7qc9v0004abcd",
"runNumber": "FASTQC-1776240000000-K3Q7M",
"status": "pending",
"pipelineId": "fastqc",
"studyId": null,
"orderId": "cm2x7q8lp0000abcd",
"targetType": "order",
"executionMode": "local"
}
}| Condition | Response |
|---|---|
Neither or both of studyId / orderId | 400 {"error":"Exactly one of studyId or orderId is required"} |
Unknown pipelineId | 400 {"error":"Invalid pipeline ID"} |
| Pipeline installed but not enabled | 403 {"error":"Pipeline fastqc is disabled"} |
| Pipeline does not support that target | 400 {"error":"Pipeline mag does not support order targets"} |
sampleIds not in the target | 400 {"error":"Invalid sample IDs: …"} |
executionMode other than default/local/slurm | 400 {"error":"executionMode must be one of: default, local, slurm"} |
| Metadata, input, or config validation fails | 400 {"error":"…validation failed","details":["…"]} |
| Target does not exist | 404 {"error":"Study not found"} / 404 {"error":"Sequencing Order not found"} |
The runNumber you get back is provisional. It is replaced with the canonical
{PIPELINE}-YYYYMMDD-NNN form when the run is actually staged — see
PipelineRun.
POST /api/pipelines/runs/[id]/start
Stages and launches the run. Facility-admin only, blocked in the demo. The body
is optional; {} or no body is fine. Recognized keys are sampleIds (only used
when the run was created without a selection) and executionMode.
The response differs by execution backend:
// SLURM
{ "success": true, "status": "queued", "jobId": "184312",
"runFolder": "/data/pipeline_runs/FASTQC-20260430-001",
"executionMode": "slurm", "warnings": [] }// Local
{ "success": true, "status": "running", "pid": 48213,
"runFolder": "/data/pipeline_runs/FASTQC-20260430-001",
"executionMode": "local",
"message": "Pipeline started in background. Check the Analysis dashboard for status.",
"warnings": [] }| Condition | Response |
|---|---|
Run is not pending | 400 {"error":"Cannot start run with status: running"} |
| Another request already claimed the launch | 409 {"error":"Run has already been started (status: queued)"} |
| Data base path or run directory unset | 400 with the specific message |
| Metadata / config validation fails at launch | 400 {"error":"…validation failed","details":[…]} and the run is moved to failed |
| Execution itself fails | 500 {"error":"<launcher message>"} and the run is moved to failed |
The 409 is not a race you need to handle by retrying — the launch claim is a
conditional update, so exactly one caller wins and a double-click can never
double-start a run.
GET /api/pipelines/runs/[id]
Run details. Requires a session plus read access to the run (below).
DELETE /api/pipelines/runs/[id]
Cancels a run — it does not delete it. Facility-admin only, blocked in the
demo. Only pending, queued, and running runs can be cancelled; anything
else is 400 {"error":"Cannot cancel a completed or failed run"}. Cancelling a
local run signals the whole process group; cancelling a SLURM run calls scancel.
Returns { "success": true, "status": "cancelled" }, or
{ "success": true, "status": "…", "alreadyFinalized": true } when the run had
already reached a terminal state in the meantime.
POST /api/pipelines/runs/[id]/delete
Actually deletes the run record and cleans up its output data. Facility-admin
only. A running run is refused with
400 {"error":"Cannot delete a running run. Cancel it first."}.
GET /api/pipelines/runs/[id]/logs
GET /api/pipelines/runs/[id]/logs?type=output&tail=200type is output (default) or error, mapping to logs/pipeline.out and
logs/pipeline.err. tail defaults to 100 lines. The endpoint prefers the
file on disk and falls back to the cached tail stored on the run — fromFile
tells you which you got. While the run is running it also parses the Nextflow
trace and returns per-process progress:
{
"content": "…last 200 lines…",
"fromFile": true,
"status": "running",
"progress": 62,
"currentStep": "Running MEGAHIT...",
"steps": [ { "process": "MAG:ASSEMBLY:MEGAHIT", "status": "RUNNING", "tasks": 4 } ]
}POST /api/pipelines/runs/[id]/sync
Reconciles a run against its execution backend — the SLURM queue or local process, plus the on-disk trace file. Requires a session and read access to the run.
// Trace file present
{ "success": true, "synced": true, "progress": 62, "processes": 7, "tasks": 41,
"currentStep": "Running MEGAHIT..." }// No trace yet (still queued, or the run never produced one)
{ "success": true, "synced": false, "message": "No trace file found yet",
"status": "queued", "queueStatus": "PENDING", "queueSource": "squeue" }400 {"error":"Run folder not set"} if the run was never staged.
Read access to a run is narrower than “owns the target”. A facility admin
can read any run. A researcher can read a run only if they own its study or
order and the run has been selected as the final result
(a PipelineResultSelection row exists for it). Otherwise the endpoint returns
403 {"error":"Forbidden"}. This applies to GET /[id], logs, and sync.
PUT /api/pipelines/runs/[id]/selection
Marks a completed run as the final result for its pipeline + target,
upserting a PipelineResultSelection. Facility-admin only (403), blocked in
the demo. This is also what makes the run visible to the researcher who owns the
target, so it is the publication step, not just a bookmark.
Returns { "success": true, "selection": { …, "selectedBy": { "id", "firstName", "lastName", "email" } } }.
| Condition | Response |
|---|---|
Run is not completed | 400 {"error":"Only completed pipeline runs can be selected as final."} |
| Run has no study or order | 400 {"error":"Pipeline run does not have a study or order target."} |
| Unknown run | 404 {"error":"Run not found"} |
Selecting a second run for the same pipeline and target silently replaces the
first — the (pipelineId, targetKey) unique constraint guarantees one winner.
DELETE /api/pipelines/runs/[id]/selection
Clears the selection, but only if this run is the currently selected one.
Returns { "success": true, "cleared": true }, or cleared: false when the
target’s selection pointed at a different run.
POST /api/pipelines/runs/[id]/resolve-outputs
Re-runs output discovery for a completed or failed run and writes the
resulting Assembly, Bin, and PipelineArtifact records. Facility-admin only,
blocked in the demo. Use it when a run finished but its results never appeared —
usually because the weblog callback was lost.
{
"success": true,
"discovered": { "assembliesFound": 1, "binsFound": 12, "artifactsFound": 4, "reportsFound": 2 },
"resolved": { "assembliesCreated": 1, "binsCreated": 12, "artifactsCreated": 4 },
"errors": [],
"warnings": []
}discovered is the adapter’s own summary. The generic manifest-driven adapter
reports the four …Found counts above; a pipeline that ships a bespoke adapter
may report a different set of keys, so treat this block as informational rather
than as a fixed schema. resolved is always the three …Created counts.
Re-running it is safe: the unique constraints on Assembly, Bin, and
PipelineArtifact make resolution idempotent.
| Condition | Response |
|---|---|
Run is neither completed nor failed | 400 {"error":"Can only resolve outputs for completed or failed runs"} |
runFolder is unset | 400 {"error":"Run folder not set"} |
| Target has no samples | 400 {"error":"No samples found for this run"} |
| No adapter and no manifest to build one from | 400 {"error":"No adapter found for pipeline: <id>"} |
GET / POST /api/pipelines/runs/[id]/pending-writebacks
The review-and-promote flow for order-scoped runs that stage read candidates
instead of writing reads directly — read cleaning is the canonical case. Both
verbs are facility-admin only (403); POST is additionally blocked in the
demo.
GET lists what is waiting:
{
"run": { "id": "…", "runNumber": "READ-CLEANING-20260430-002", "pipelineId": "read-cleaning",
"status": "completed", "orderId": "…" },
"readCandidates": [
{ "artifactId": "…", "outputId": "cleaned_reads", "outputLabel": "Cleaned reads",
"sampleId": "…", "sampleCode": "S1",
"file1": "…/S1_R1.trimmed.fastq.gz", "file2": "…/S1_R2.trimmed.fastq.gz",
"readLayout": "paired", "targetDataClass": "cleaned", "status": "candidate",
"currentRead": { "id": "…", "file1": "…", "file2": "…", "dataClass": "raw",
"dataClassLabel": "Raw / protected", "isProtectedRaw": true } }
],
"reports": [ { "id": "…", "name": "multiqc_report.html", "path": "…", "outputId": "qc_summary" } ],
"review": { "title": "Review pending read outputs", "…": "UI copy strings" }
}POST promotes them. Optional body { "sampleIds": ["…"] } scopes the
promotion; omit it to promote every candidate that is not already promoted.
{ "success": true, "promoted": 2, "readIds": ["cm2x7qd…", "cm2x7qe…"] }Promotion copies the candidate files into the order’s canonical read location,
creates a new active Read, and supersedes the previous active read rather than
deleting it. A candidate can never claim a protected data class — see
Read.
| Condition | Response |
|---|---|
| Run is not order-scoped | 400 {"error":"Pending read promotion requires an order-scoped run"} |
| Nothing left to promote | 400 {"error":"No pending read candidates selected for promotion"} |
| Unknown run | 404 {"error":"Pipeline run not found"} |
GET / POST /api/pipelines/runs/[id]/cleaned-reads
The narrower, read-cleaning-specific variant of the same review flow, kept for
the dedicated cleaned-reads panel. Same auth (403 for non-admins, POST
blocked in the demo) and the same { "sampleIds": [...] } body. Errors follow
the same convention: 404 when the message contains “not found”, 400
otherwise.
Pipeline weblog
Nextflow runs POST their execution events to this endpoint. It also accepts events from other adapters (SLURM trace, queue polling, internal step transitions). See Pipeline Runtime for the client-side configuration.
POST /api/pipelines/weblog?runId=<id>&token=<secret>
The target run is identified by the runId query parameter, and the call is
authenticated by the shared weblog secret in the token query parameter. There
is no session cookie and no runId in the body.
| Condition | Status |
|---|---|
| No weblog secret configured on the instance | 503 {"error":"Weblog secret is not configured"} |
token does not match | 403 {"error":"Invalid token"} |
Missing runId | 400 {"error":"runId is required"} |
Unknown runId | 404 {"error":"Run not found"} |
The secret is set under Admin → Application Settings → Pipeline execution.
Until it is set the endpoint fails closed with 503, so this state-mutating
webhook is never left unauthenticated.
curl -X POST "https://your-host/api/pipelines/weblog?runId=cm2x7qc9v0004abcd&token=$WEBLOG_SECRET" \
-H "Content-Type: application/json" \
-d '{
"event": "process_completed",
"trace": { "process": "MAG:ASSEMBLY:MEGAHIT", "status": "COMPLETED", "exit": 0,
"duration": "00:43:12", "%cpu": "812", "rss": "12.3 GB" }
}'On success the endpoint returns { "success": true }.
How a payload is interpreted
The event name is read from event, eventType, or type, lowercased, and
matched by substring — workflow_complete, process_start,
process_completed, workflow_error and their variants all work. Process and
trace details come from trace, falling back to task. The event timestamp is
taken from utcTime, timestamp, or the trace’s complete/start/submit
fields, and is clamped to the receipt time if it is more than 6 hours in the
future or more than 30 days in the past.
Behaviour worth knowing before you build an adapter:
- Terminal runs are never resurrected. Once a run is not in
pending,queued, orrunning, a lateworkflow_completeno longer changes its status or timestamps. The event row is still recorded. - A failed process is not a failed run.
errorStrategy 'ignore'is legitimate, so aprocess_failedevent setscurrentStepand leaves the run active; only a workflow-level event finalizes it. - Completion waits for outputs. On
workflow_completethe endpoint first checks the scheduler: if the job is still active the run staysrunningat 99%. Otherwise it resolves outputs, and only marks the runcompletedif that succeeded. - Duplicate suppression. An event identical to one already stored within ±2 seconds is dropped.
- Retention. Only the newest 100 events per run are kept; older rows are deleted in the same transaction.
- Payload truncation. The stored
payloadis capped at 12,000 characters andmessageat 500.
Event sources
PipelineRunEvent.source records where an event came from. This endpoint always
writes weblog; the other values are written by internal code paths.
| Source | Meaning |
|---|---|
weblog | Nextflow -with-weblog payload posted here |
trace | Parsed trace.txt row, written by sync or the monitor |
queue | SLURM queue poller, e.g. a job moving PD → R |
process | Internal step lifecycle event |
launcher | Written at launch, e.g. preparation_warning |
eventType is not gate-kept — an unrecognized value is stored verbatim, so a
custom adapter can define its own.
Sequencing run import
POST /api/orders/[id]/sequencing/runs/import
Facility-admin only. Accepts a multipart form with an Excel attachment under
the field name file, describing a run plan of (run, sample, barcode) rows.
This is what the Import Excel button on the sequencing tab calls. CSV is
not accepted — the body is parsed with ExcelJS.
Limits: 5 MB, 1000 data rows, 80 columns. Exceeding the size limit returns
413; the others return 400.
The worksheet is resolved by name, in order: Run Samples, Samples,
Tabelle2, then the first worksheet with more than one row. Header cells are
normalized (lowercased, punctuation stripped, µ folded to u) and matched
against an alias table, so real-world spreadsheets usually map without editing:
| Spreadsheet header (any of) | Mapped field |
|---|---|
| Run, Run ID, Run name, Sequencing run | runId |
| Sample, Sample ID, Sample code, Patient, Patient ID, Internal sample code | sampleCode |
| Barcode, Barcode ID, Barcode name | barcode |
| Material, Body site | material_body_site |
| Date, Sampling date | sampling_date |
| DNA ng/µl, Concentration ng/µl | concentration_ng_ul |
| Storage box, Storage position, Buffer | storage_box, storage_position, storage_buffer |
Unrecognized columns are not dropped — they come back in unmappedColumns and
per row in unmapped, so you can see what was ignored.
This is a two-phase flow. Without ?apply=true the endpoint returns a preview
and never mutates anything:
{
"sheet": "Run Samples",
"rows": [ { "rowNumber": 2, "runId": "RUN-2026-04-30-001", "sampleCode": "S1",
"barcode": "BC01", "customFields": { "material_body_site": "stool" },
"unmapped": { "Notes": "repeat extraction" } } ],
"rowCount": 1,
"unmappedColumns": ["Notes"],
"missingSamples": ["S99"],
"duplicateBarcodes": [ { "runId": "RUN-2026-04-30-001", "barcode": "BC01", "count": 2 } ],
"rowErrors": [ { "rowNumber": null, "message": "Sample not found on this order: S99" } ],
"applyReady": false
}applyReady is true only when there is at least one row and rowErrors is
empty. Rows missing a run or sample column produce a row-level error; missing
samples and duplicate barcodes produce errors with rowNumber: null because they
are detected across the whole sheet.
With ?apply=true and a clean preview, the endpoint upserts SequencingRun and
SequencingRunSample rows and returns the preview fields plus:
{ "success": true, "createdOrUpdated": [ { "runId": "RUN-2026-04-30-001", "assignments": 2 } ] }Applying also splits the mapped custom fields: fields that belong to the ONT
sample field set are merged into Sample.customFields, and everything else is
stored on the SequencingRunSample row. A field like sampling date therefore
follows the sample, while a run-specific note stays with the assignment.
Sending apply=true while rowErrors is non-empty returns 400 with
{"error":"Import contains rows that need review before saving", …preview} — the
full preview is echoed so a client can render the problems without re-uploading.
Live stream (MinKNOW ingest)
These endpoints drive live Oxford Nanopore ingest. The read endpoints accept a facility-admin read session (demo sessions may look); the mutating ones require a full facility-admin session. See the stream data models.
GET /api/orders/[id]/stream
The order’s stream runs, newest first, up to 50, each with its most recent event.
totalBases is serialized as a string because it is a BigInt. This is the
list behind the Stream page’s run history
strip, so stopped runs in the response are readable in the interface; runs older
than the 50 returned here are reachable only through this API.
{
"runs": [
{ "id": "cm2x7qf…", "orderId": "cm2x7q8lp0000abcd", "minknowRunId": null,
"flowCellId": "FAX12345", "deviceId": "MN12345",
"outputDir": "/data/minknow/exp1/no_sample/20260430_1200",
"status": "ACTIVE", "totalBases": "18342190231", "totalReads": 412003,
"barcodeMap": { "barcode01": "S1" },
"startedAt": "2026-04-30T12:00:00.000Z", "lastSeenAt": "2026-04-30T13:42:11.000Z",
"stoppedAt": null,
"latestEvent": { "kind": "FILE_INGESTED", "ts": "2026-04-30T13:42:11.000Z", "payload": { } } }
]
}POST /api/orders/[id]/stream
Starts a stream run. outputDir is required and must resolve — after symlink
resolution — under the configured MinKNOW output root. That containment check
runs once, here; from then on the boundary is held by the monitor’s watcher,
which does not follow symlinks. Optional: deviceId, flowCellId,
minknowRunId, and a barcodeMap object (keys are lowercased on write).
Returns 201 with { "id": "cm2x7qf…" }.
| Condition | Response |
|---|---|
outputDir missing | 400 {"error":"outputDir is required"} |
outputDir outside the configured root, or unresolvable | 400 with the validation reason |
Another ACTIVE run already watches that directory | 409 naming the conflicting run and order |
The conflict check, the run insert, and the RUN_STARTED event run in one
SERIALIZABLE transaction, so two simultaneous starts cannot both succeed — the
loser also gets a 409.
GET /api/orders/[id]/stream/[streamRunId]/events
Cursor-paginated event feed. Without a cursor it returns the newest events (the
initial live-tail load); with ?after=<seq> it pages forward from that sequence.
?limit=<n> is clamped to [1, 500], default 100. Events are always presented
newest-first regardless of paging direction.
{
"events": [ { "id": "…", "seq": 42, "ts": "2026-04-30T13:42:11.000Z",
"kind": "FILE_INGESTED", "payload": { "barcode": "barcode01", "reads": 4000 } } ],
"cursor": 42
}Advance your client cursor to the returned cursor and poll again. Because
paging is by the monotonic seq and forward pages are fetched oldest-first, a
backlog larger than limit is delivered incrementally instead of skipping the
middle.
A run id that does not belong to this order returns
404 {"error":"Stream run not found"}.
POST /api/orders/[id]/stream/[streamRunId]/stop
Requests a stop. This is a soft stop: the API sets the run to STOPPING and
emits a RUN_STOP_REQUESTED event; the monitor daemon closes its watcher on the
next tick and writes STOPPED. The API never touches the watcher, which lives in
a different process.
Returns { "ok": true }, or { "ok": true, "alreadyStopped": true } /
{ "ok": true, "alreadyStopping": true } when the run was already there.
GET /api/orders/[id]/stream/[streamRunId]/by-barcode
Aggregates the run’s FILE_INGESTED events by barcode, sorted by barcode.
Files whose event carried no barcode are grouped under (unknown).
{
"barcodes": [
{ "barcode": "barcode01", "fileCount": 12, "totalSize": 1048576, "totalReads": 4000,
"totalBases": 1200000, "lastFileAt": "2026-04-30T13:42:11.000Z",
"lastFilePath": "/data/minknow/…/fastq_pass/barcode01/FAX12345_pass_barcode01_12.fastq.gz" }
]
}Troubleshooting
| Symptom | Likely cause |
|---|---|
401 {"error":"Unauthorized"} on every call | No session cookie. There is no API key — sign in and reuse the cookie jar. |
403 {"error":"Unauthorized"} on a write | You are authenticated but not a FACILITY_ADMIN. |
403 "… disabled in the public demo." | You are on a demo session; demo workspaces block every mutating operation. |
Weblog returns 503 | No weblog secret configured. Set it under Admin → Application Settings → Pipeline execution. |
Weblog returns 403 | Secret mismatch. The value in the run’s generated nextflow.config was written when the run was staged — regenerate the run after changing the secret. |
400 {"error":"Data base path not configured"} | site.dataBasePath is unset in settings.json, the environment, and the database. See Data Storage. |
400 "Sequencing data can only be managed on submitted or completed sequencing orders" | The order is still DRAFT. |
Upload PATCH keeps returning 400 about the offset | Your offset drifted. GET the upload session or re-read receivedSize from the last successful chunk response and resume from exactly that byte. |
Run finished on the cluster but SeqDesk still shows running | The weblog could not reach the app — common when compute nodes are network-isolated. Call POST /api/pipelines/runs/[id]/sync, then resolve-outputs if artifacts are missing. |
| A researcher cannot see a completed run | The run has not been selected as the final result. PUT …/selection publishes it. |
Conventions
- POST bodies are
application/jsonunless the endpoint takes multipart (the run-plan import) or a raw binary body (upload chunks). - Errors are
{ "error": "<message>" }with a 4xx/5xx status. Validation errors that have several causes adddetails: [ … ]. - BigInt columns (
totalBases,size,expectedSize) are serialized as strings or numbers depending on the endpoint — check the example rather than assuming. - Pagination is
?limit=&offset=for run listings and?after=<seq>&limit=for the stream event feed. There is no global pagination convention. - Sequencing data management, run control, and result promotion require
FACILITY_ADMIN. Read-only run endpoints additionally admit the researcher who owns the target, but only for published runs.