@myelinbridge/cli
v0.11.1
Published
Myelin Partner Ingestion CLI — push R&D data deliveries from a pipeline: preflight against the client's quality rules, resumable upload, submit, track review outcomes.
Downloads
1,101
Maintainers
Readme
@myelinbridge/cli
Push R&D data deliveries into Myelin from a pipeline — preflight against the client's published quality rules, resumable upload, submit, and track review outcomes. Full API reference: https://myelinbridge.com/developers.
Quick start (~10 minutes from key to first submit)
export MYELIN_API_KEY=myl_live_… # created by your bridge owner in Bridge → API
npx @myelinbridge/cli ping # verifies auth, prints your projects
npx @myelinbridge/cli datasets # what you can deliver to, and whose move it is
npx @myelinbridge/cli contract --dataset onco1-wes # what is expected of your delivery
npx @myelinbridge/cli sample-depth 1 --dataset onco1-wes # once, before your first submit
npx @myelinbridge/cli roles --dataset onco1-wes # which file plays which role
npx @myelinbridge/cli roles set samplesheet /metadata/samplesheet.csv --dataset onco1-wes
npx @myelinbridge/cli check ./run_042 --dataset onco1-wes # validate BEFORE uploading a byte
npx @myelinbridge/cli push ./run_042 --dataset onco1-wes --submitDeclare your sample depth once, first. The client creates and describes the dataset; you own your output structure, so you tell Myelin at which folder depth a sample sits —
0= the delivery root is one sample,1(default) = each top-level folder is a sample,2= one level deeper. Sample-scoped quality checks group by it, so getting it right up front is what makes per-sample verdicts mean anything. It is idempotent (safe to assert on every pipeline run, before and after the lock) and locks once your first batch leaves draft, so that verdicts stay comparable across deliveries — after that, re-asserting the current value still succeeds and only a change exits2. This is the only dataset field you can write.Declare which file plays which role — whenever you like (0.11.0). Quality checks read files by ROLE (
samplesheet,checksum_manifest,qc_report,subject_roster,capture_bed), not by a filing convention you never agreed to.rolesshows what is declared and what Myelin detected in your deliveries;roles set <role> <path>confirms a file;roles none <role>states your dataset has no such file (a statement, not a gap). Nothing is ever required to deliver — an undeclared role just means the checks that read it report "could not check" instead of running, so declaring is how you activate more checks before upload and catch problems before review does. Unlikesample-depthit never locks, and it is idempotent, so a pipeline can assert it on every run. The samplesheet is sticky: send it once and every later delivery reconciles against it — a partial delivery (93 of 96) is reported as a state, never as a failure.A missing path in
roles setis refused, not guessed (0.11.1). Flags are not positional arguments:roles set samplesheet --dataset onco1-wes(path omitted) used to declare the samplesheet at the literal path--datasetand print a tick. Every samplesheet check then reported "could not check — the declared file is not in the delivery" on a delivery that was fine. The command now exits with its usage line instead.--datasettakes the slug in any case (0.11.0). Slugs are matched case-insensitively (onco1-wesandONCO1-WESname the same dataset); an id or an exact name works too.Resume = re-run.
pushis idempotent: already-uploaded files are skipped (path + size), and within a large file, parts that already landed are skipped too (S3 multipart). Uploads go direct to storage over short-lived presigned URLs — no credential is stored on your machine, and revoking the API key cuts off signing immediately.Transient failures retry themselves. Since 0.8.0 a rate limit (
429) waits outRetry-Afterand retries on every command, and a part upload that hits a storage hiccup (5xx, an edge timeout, an expired URL) re-signs, backs off, and retries up to 4 attempts before surfacing — an unattended pipeline run survives a blip. A retried--submitwhose first attempt actually landed gets a success echo (already_submitted: true), not a false failure.Slow connections shrink their parts. Since 0.9.0, when the same part dies twice on a timeout (the edge kills any PUT that runs too long — HTTP 524),
pushrestarts the file with smaller parts, 64 → 16 → 5 MB, instead of retrying the same slice into the same wall. A hospital-grade uplink down to roughly 30 KB/s can now complete a delivery; it is slower, but it finishes.Every file is fingerprinted. Since 0.7.0,
pushcomputes an MD5 of each file while it uploads and records it with the delivery. If the client's quality contract includes a checksum-manifest check, your delivery verifies against your ownmd5_manifest.csvinstead of reading as "unverifiable".Read the contract before you build the delivery.
contractprints what the client expects — every check as one plain sentence, grouped by what it answers (completeness, structure, validity, consistency, integrity, privacy), and marked!when a failure blocks validation or?when the client's reviewer confirms it rather than asking you to fix it. It also tells you which checkscheckcan verify locally and which only run once the files are uploaded, so nothing about the bar is a surprise at review time.checkcosts nothing. It evaluates your local file list against the dataset's quality checks server-side — same engine, same verdicts as submit — without uploading. Exit code 2 means the delivery would not be validatable as it stands: a blocking rule fails, or a blocking rule could not be evaluated at all.A check has five answers, not three (since 0.10.0). Alongside passed, flagged and failed, the engine can now say it could not check something (a missing precondition, or a rule it cannot evaluate) and that a rule does not apply to your delivery. This matters to you in one specific way: a rule the client wrote badly used to come back as a failure on your delivery. It now says the rule could not be evaluated, and tells you it is not yours to fix.
checkprints the count it could not check and what each one needs — supply it and those checks run on your next delivery, which is fewer rejections later.Some checks are confirmed, not fixed. A rule marked needs confirmation asks nothing of you: the client's reviewer records what they decided and the delivery proceeds.
checkreports these and never exits 2 on them.The fix loop is machine-readable. On
changes_requested,myelin status <batch> --jsonreturns the failed files, reviewer comments, and rule remediation hints; fix, re-push --submit, unchanged files keep their review votes.Limits.
pushdeclares 500 files per call and chunks automatically — nothing for you to split.checksends your whole file list in one preflight call, which accepts at most 10 000 files — a larger delivery cannot be preflighted bychecktoday (the API answers400 too_many_files); its checks still run at submit. A delivery holds at most 25 000 files (submit refuses above that with422 submit_blocked), and a checksum manifest is inspected up to 16 MiB.
If you are the client, not the partner
Two kinds of key exist, and they are not interchangeable. Everything above needs a partner key (write: upload, submit). A client key is read-only and answers the question your own systems ask once the data has landed: the bucket is full of UUIDs — what is this?
Where they live follows from what they are. A partner key belongs to one bridge, because a partner does. A client key belongs to your organisation: one key, every partner, one answer. Your organisation admin creates it in Organisation → API keys. To narrow one to a single partner's data, scope it to that partner's projects.
export MYELIN_API_KEY=myl_live_…
# What has landed, newest first (page with --cursor from the printed next_cursor)
myelin deliveries --dataset <dataset-id>
# What is this object, exactly?
myelin resolve gs://acme-landing/inbox/acme-cro/4319…/de99…/7aa5…/data/SAMPLE_01/reads.fastq.gz
# file · gs://…/reads.fastq.gz
# project ONCO1 — Oncology discovery
# dataset WES batch 7 (Genomics)
# batch ONCO1-WES-007 · #7
# validated 2026-08-07T18:05:12Z · delivered 2026-08-07T18:06:20Z
# manifest gs://…/7aa5…/_myelin/manifest.json
# file reads.fastq.gz · 4096 bytesresolve accepts a full gs:///s3:// URI, a bare prefix, or a single id, and answers at
whatever granularity the path supports.
Each delivery also carries the same record as a file, written next to the data at
_myelin/manifest.json (plus _myelin/files.csv, a flat table you can load straight into a
warehouse). Prefer the file for anything auditable: it is frozen at delivery time, needs no
credentials, and does not depend on Myelin being reachable. Use the API when you want it live.
A partner key calling these gets 403 wrong_key_side, and vice versa.
Machine mode
Every command takes --json. Exit codes: 0 ok · 1 error · 2 blocked
(blocking preflight failure, a blocking rule the engine could not evaluate,
locked delivery, blocked submit, locked sample depth).
check --json carries the numbers to branch on rather than the prose:
| Field | |
|---|---|
| blocking_failures | Blocking rules that were evaluated and failed. Unchanged meaning since the first release. |
| blocking_not_evaluated | Blocking rules the engine could not evaluate. New in 0.10.0 — these stop the reviewer validating just as surely, which is why check now exits 2 on them too. |
| must_acknowledge_failures | Rules the client asked to be told about. Reported, never exited on. |
| counts | passed, flagged, failed, not_evaluated, not_applicable (which sum to evaluated), plus deferred, manual and checks (evaluated + deferred + manual === checks). |
Every per-check result carries details.abstained when the engine did not
conclude: rule means the client's rule could not be evaluated and there is
nothing on your side to fix; data means something the check needs is missing
from the delivery; nothing_to_evaluate means the rule matched none of your
files. Branch on that before you page anyone.
API errors carry structure, not just prose: in --json mode an error is
{ "error", "code", "status", "request_id" } — branch on code, and quote the
request_id when reporting a problem (it is printed in human mode too; it lets
Myelin find the exact server-side log line). myelin version prints the CLI
version.
Environment
| Variable | |
|---|---|
| MYELIN_API_KEY | Required. Created by your bridge owner in Bridge → API. |
| MYELIN_API_URL | Optional. Defaults to https://myelinbridge.com/api/v1. |
| MYELIN_API_HEADER | Optional. Extra headers sent with every API call, one Name: value per line — for a Myelin deployment fronted by something that authenticates before Myelin does (a corporate gateway, an SSO-protected preview). It can never override Authorization. |
Webhooks instead of polling
Register an HTTPS endpoint (portal Bridge → API, or POST /v1/webhook-endpoints)
to receive signed events (batch.validated, batch.changes_requested,
batch.transferred, …). Verification snippets: https://myelinbridge.com/developers.
Polling fallback: GET /v1/events?cursor=….
