Skip to main content

Tasks

Six tasks, each its own published operationPOST /v1/tasks/promoter/predict, /v1/tasks/splice/predict, /v1/tasks/enhancer/predict, /v1/tasks/chromatin/predict, /v1/tasks/annotation/predict, /v1/tasks/expression/predict — plus two workflows that compose them. The URLs are exactly the strings you already POST to; what changed is that each now has its own request schema, its own minLength, and its own closed options object, instead of one shared PredictRequest. (Regenerate typed clients built against the old shared model.) The same six tasks are also tools over MCP.

Each accepts a DNA sequence (and a sequence_name label) and returns task-specific fields under data; meta is uniform across tasks. Request and response schemas — with copyable examples — are in ReDoc; per-task length caps and latency are in Limits.

TaskInput (bp)Model windowStrand-sensitiveOutput
Promoter300–500,0002,000 (300 on the *-300bp models)Yes (coding strand)Promoter-region probabilities
Splice100–500,00015,000Yes (transcript orientation)Donor / acceptor sites
Enhancer50–500,000249Yes (dev/hk channels)dev + hk activity scores
Chromatin200–500,0001,000No919 features in 8 track groups
Expression9,198–500,000 (+ tss_index)— (trained_window_bp 9,198)Yes (coding strand)Expression in log(TPM+1)
Annotation1,000–500,000Plus-strand inputTranscripts with type + exon/intron/CDS structure (GFF3)
Find genes + expression1,000–500,000Plus-strand inputGenes found, each with a predicted expression
Variant interpretationA VCF, not a sequenceCoordinates, not strandAn annotated copy of your VCF, written back to your bucket

Where a task is marked strand-sensitive, submit DNA 5'→3' on the gene's coding (sense) strand; for annotation, submit the plus-strand region.

GET /v1/tasks/{task}/models returns {task, default_model, models}; each model's is_default marks the default. The model-list endpoint is authoritative for what each task currently serves.

The "input" column is the accepted range, enforced before any model loads. The "model window" is the model's own context_window_bp: a sequence above the floor but below that window is still scored — against a window padded out to it. See the floor is admission control, not regime.

Model selection

Each task has a default_model and may offer alternatives. List them with GET /v1/tasks/{task}/models (a flat {task, default_model, models} object, not the {data, meta} envelope; it needs the same bearer key, and an unknown task is 404 not_found). To pick one, pass its id in the top-level model field of the request bodynot options.model: every task's options object is closed, so an unrecognised key there is 422 validation_failed with loc ["body","options","model"].

Each model's bio_spec carries request_max_bp (the enforced ceiling), context_window_bp (its own window) and trained_window_bp (a fixed receptive field where it has one) — see Limits. Beyond the default human models, promoter offers species-specific variants (Drosophila, yeast, Arabidopsis), and DNABERT k-mer variants exist for promoter, enhancer, and chromatin. The model-list endpoint is authoritative for what each task currently serves; the same roster and default are also published statically as x-models on each predict operation in /v1/openapi.json, for tooling that wants it without a live call.

Options

options is typed and closed per task: unknown keys are rejected with 422 validation_failed (type: extra_forbidden) rather than ignored. The complete set:

Taskoptions keysRequired
Promoterthreshold (0–1, default 0.5)
Splicethreshold (0–1, default 0.5), site_types (subset of ["donor","acceptor"], default both)
Enhancernone — the object accepts no fields
Chromatinthreshold (0–1, default 0.5)
Annotationbatch_size (1–128, default 8), shift_coordinates (string, e.g. "UCSC"), reverse_complement (bool, default true)
Expressiondescription (string)description, and options itself
Find genes + expressiondescription, annotation_model, expression_model, batch_size (1–128, default 8), shift_coordinatesoptions itself; description enforced at runtime

Promoter

Binary classification that detects promoter regions. The default g0-promoter-2000bp is a G0 BERT-Large encoder trained on human promoters from EPDnew. Inputs above 2,000 bp are scanned in windows automatically.

  • sequence: floor set by the smallest context_window_bp among the promoter models (bounds in the table above); below g0-promoter-2000bp's own 2,000 bp window the input is padded out to it.
  • options.threshold (0–1, default 0.5): probability cutoff for calling a window a promoter. The only key PromoterOptions accepts.
  • Strand-sensitive — submit the coding (sense) strand.

Output: per-window promoter probabilities. Also BED and bedGraph?format=json|bed|bedgraph (see output formats).

MCP: "Fetch a 100 kb window around human TP53 and scan it for promoter regions."

Splice

Token-level classification labeling each position as a splice acceptor (3' site), donor (5' site), or neither. The default g0-splice-bigbird is a G0 BigBird encoder trained on human splice sites from SpliceAI. Long inputs are scanned in a 15,000 bp sliding window; predictions are most reliable in the central region of each window.

  • sequence: the floor (bounds in the table above) is well below the model's 15,000 bp window, so short submissions are scored against heavy padding — treat 15,000 bp as the in-regime size.

  • options.threshold (0–1, default 0.5): score cutoff for emitting a site. A response carrying more than 20,000 sites is refused with 422 validation_failed — only a very low threshold on a long sequence gets there. If you want everything, use 1e-3, not 0: it is numerically equivalent (no score moved by more than 4.6e-4 in our benchmark), ~4x faster, and stays under the cap. See Limits.

  • options.site_types: subset of ["donor", "acceptor"] (default both). With threshold, the only two keys SpliceOptions accepts.

  • Strand-specific — submit the transcript's orientation. The wrong strand does not fail loudly, and it does not return near-zero scores either: it returns sites at different positions, often still at high confidence. On our BRCA1 fixture the reverse complement scored 0.9101 and 0.9946 where gene-sense scored 0.9998 and 0.9999; on SMN1 it returned 1 site instead of 17. Neither the site count nor the scores give you a usable check, and there is no strand flag in the contract, so track orientation on your side.

  • start/end bound the scored token, not the junction. The model reads your sequence as BPE tokens and scores whole tokens, so a site's span is that token's character range — typically 4–8 bp, and varying from site to site because token lengths do. A donor or acceptor junction is a single base. token_index names the token the span came from. Do not treat the span as the feature's extent, and be careful intersecting it with a base-resolution reference annotation: at that scale it will be quietly wrong. We publish no derived base position — deriving one from a token score would be a modelling claim we cannot currently defend, and invented precision in a file you keep is worse than an honest span. Stated in the contract since revision 5.

Output: detected acceptor and donor sites with scores. Also BED and GFF3?format=json|bed|gff3 (see output formats). The GFF3 source column carries the model id (g0-splice-bigbird); it read gpu_service before contract revision 4. Columns 4–5 of that GFF3 are the token spans described above, so a saved track has token resolution, not base resolution.

MCP: "Fetch the human HBB gene sequence and predict its splice sites."

Enhancer

Regression predicting two scores per window: developmental (dev) and housekeeping (hk). The default g0-deepstarr is a G0 BERT-Base encoder trained on Drosophila enhancers from DeepSTARR. The sequence is split into consecutive, non-overlapping 249 bp windows, each scored independently.

  • sequence: the floor (bounds in the table above) is admission control — the shortest input g0-deepstarr will admit — not a useful size: the model scores 249 bp windows, so anything shorter is accepted and scored against a padded window. Submit ≥ 249 bp to stay in regime.
  • options: EnhancerOptions accepts no fields. Send {} or omit it; any key is a 422. There is no threshold on this task — it is regression, not classification.

The model is Drosophila-trained and strand-sensitive for the dev/hk channels — use a fly sequence on the gene's coding (sense) strand.

Output: per-window dev and hk scores, embedding bedGraph renderings of each track. bedGraph is also available directly — ?format=json|bedgraph (see output formats).

MCP: "Fetch the Drosophila ftz gene and predict enhancer activity."

Chromatin

Multi-label classification predicting 919 mammalian chromatin features (DeepSEA-style), grouped into eight categories (DNase, CTCF, Pol2, c-Myc, H3K27ac, H3K27me3, H3K4me1, Other). The default g0-deepsea is a G0 BERT-Base encoder trained on ENCODE data. The sequence is tiled into 1,000 bp windows stepping every 200 bp.

  • sequence: the model's window is 1,000 bp (floor in the table above), so submit at least that to avoid padding.
  • options.threshold (0–1, default 0.5): only features scoring above this are returned. The only key ChromatinOptions accepts.

Output: per-feature probabilities (above the threshold), grouped by category. Also BED?format=json|bed (see output formats).

MCP: "Fetch the human HBB promoter region and predict its chromatin features."

Expression

Predicts gene expression from a DNA sequence plus an experimental-context description, on a log(TPM+1) scale. The default g0-expression is trained on ENCODE RNA-seq across cell types.

Expression has its own operation and request schema — POST /v1/tasks/expression/predict, ExpressionPredictRequest — as every task now does. It remains the only one that takes a tss_index and the only one whose options has a required field.

The body accepts exactly five fields — sequence (required), options (required), tss_index, sequence_name, model — and is closed: any other top-level key returns 422 validation_failed.

  • sequence (required): 9,198–500,000 bp, gene-sense strand. The model scores exactly one 9,198 bp window (4,599 bp each side of the TSS) — shorter input is a hard 422, never padded or truncated. Lengths are counted after whitespace is stripped, so a line-wrapped FASTA body pastes verbatim (the > header line does not).

  • tss_index (integer, 0-based): offset of the TSS into the whitespace-stripped sequence. Required unless sequence is exactly 9,198 bp, where it defaults to 4599. Must satisfy 4599 <= tss_index <= len(sequence) - 4599. The server scores sequence[tss_index - 4599 : tss_index + 4599]. It does not discover the TSS for you and does not reverse-complement. See Limits for the full rules and the silent-wrong-window gotcha.

  • options.description (required): free-text experimental context (cell type, assay, conditions). options is a closed object — description is its only accepted key, and it must be non-empty. Omitting it returns 422 validation_failed.

    This string is model input, not a label. It conditions the prediction, and the exact wording changes the number: on one fixture, same sequence and same model, "K562" scored 1.38 and "K562 cells" scored 0.70 log(TPM+1) — roughly a 2x spread in TPM from phrasing alone. There is no enum and no canonical vocabulary. Pick one form and keep it fixed: when you compare predictions across sequences, runs, or releases, a changed description invalidates the comparison as surely as a changed sequence.

Output: data.prediction = {expression, expression_log_tpm, expression_tpm, unit}expression/expression_log_tpm are log(TPM+1); expression_tpm is the back-transformed linear value. JSON only (no text-track formats; ?format= anything but json is 415 unsupported_format).

Windowing provenance is echoed back in two places: meta.task_specific_counts (and data.input) carry tss_index and scored_window — the half-open [start, end] of your submitted sequence that was actually scored, always exactly 9,198 bp wide. The length you submitted (whitespace-stripped) is meta.sequence_length, also echoed as data.input.submitted_sequence_length; the scored width is always 9,198 bp, which is scored_window[1] - scored_window[0]. (data.input.sequence_length was removed at contract revision 13.)

MCP: "Predict HBB expression in K562 cells."

Annotation

Gene finding over long DNA: detects transcripts and returns their intervals. The g0-annotation model (multispecies) predicts transcript boundaries, transcript type (mRNA / lnc_RNA), and full internal exon/intron/CDS structure, and emits a browser-ready GFF3 track. Submit the plus-strand genomic region; the model finds transcripts on both strands. This is the longest-running task, and the only predict operation with a hard sync cap: above 200,000 bp a synchronous JSON request is refused with 413 sync_too_large, details = {sequence_length, threshold} — resend the identical body with Prefer: respond-async. Below that, sync works at any accepted length, but prefer async above ~30,000 bp (see Asynchronous jobs). The cap is on JSON delivery only: ?format=bed and ?format=gff3 are served synchronously at any accepted length, so size those calls against the 300 s proxy timeout yourself.

  • sequence: the highest floor of the five non-expression tasks (bounds in the table above); the gene finder cannot work below its own window.
  • model (optional, top-level): g0-annotation — the only annotation model, selected by default.
  • options.batch_size (integer 1–128, default 8): windows processed per batch.
  • options.shift_coordinates: "UCSC": rebase output coordinates from a UCSC-style sequence_name (e.g. chr8:127,680,000-127,800,000). Note that the header goes in sequence_name (≤ 128 chars), not in the sequence — a > line inside the sequence fails the alphabet check.
  • options.reverse_complement (bool, default true): average each locus with its reverse complement, improving minus-strand recall. Set false for a faster single-pass run.

AnnotationOptions accepts those three keys and nothing else.

Output: transcript intervals, each carrying its own strand (+/-) plus tss_position and polya_position. Each transcript additionally carries transcript_type, transcript_type_score, and 0-based half-open exons/introns/cds arrays, and data.formats.gff3 holds a full GFF3 track (also via Accept: text/x-gff3). BED is also available — ?format=json|bed|gff3. See output formats.

MCP: "Find the genes in chr8:127,680,000-127,800,000."

Find genes and predict expression

POST /v1/workflows/find-genes-and-predict-expression — a composite that chains the two tasks server-side: annotate the region, centre a 9,198 bp window on each found gene's TSS, and score expression for each. One round trip, no intermediate results shuttled back to your client, and no tss_index to compute.

It is a published operation on the same bearer key as everything else; nothing needs enabling.

  • sequence: 1,000–500,000 bp — annotation's floor, since the workflow cannot find a gene in less sequence than the gene finder looks at.
  • options (required), and options.description (required — the expression stage is conditioned on it; missing or empty is 422 validation_failed, "options.description is required (cell type / assay context)"). Also accepts annotation_model, expression_model, batch_size (1–128, default 8), shift_coordinates. Closed, like every other options.
  • Above 50,000 bp synchronous delivery is refused with 413 sync_too_large, details = {sequence_length, threshold}. Resend the identical body with Prefer: respond-async and poll GET /v1/tasks/jobs/{job_id} (see Asynchronous jobs). This is the lower of the contract's two sync caps; annotation carries the other, at 200,000 bp.

Output: data.expression_predictions[], one entry per gene found; a gene the workflow could not score carries a skip_reason. meta.task_specific_counts = {genes_found, genes_predicted, genes_skipped}, with genes_predicted + genes_skipped == genes_found. Unlike the direct expression endpoint, the workflow pads a TSS window with N up to half its width rather than dropping a gene near the edge of your region. JSON only.

MCP: "Find the genes in chr8:127,680,000-127,800,000 and predict each one's expression in K562."

Genomic variant interpretation

POST /v1/workflows/genomic-variant-interpretation — the one operation that does not take a DNA sequence. You give it the s3:// URI of a VCF in your own object storage; it scores the variants that fall near a transcription start site and writes an annotated copy beside it, named after it. The JSON you get is a receipt: where the annotated file is, and how the run went. You never parse a VCF to drive the workflow, and the annotation format inside it can change without breaking anything you built.

Under development

This workflow is not finished. The contract below is real and will not change silently, but the per-tissue values in an annotated file are not final and are not yet to be read as results. A run is model output when, and only when, the response carries meta.model and the annotated VCF carries a ##GEXModel header line — until both appear, treat the numbers as format rather than findings. It also needs a grant on your bucket before it will run for you, so talk to us rather than integrating against it unannounced.

Asynchronous only. A whole-exome case runs for hours, well past any proxy timeout, so Prefer: respond-async is required; a request without it is 400 bad_request. There is no callback — submit, then poll GET /v1/tasks/jobs/{job_id} (see Asynchronous jobs).

POST /v1/workflows/genomic-variant-interpretation
Authorization: Bearer gi_...
Prefer: respond-async
Content-Type: application/json
{
"input": { "type": "s3_object", "uri": "s3://your-bucket/cases/CASE-00123/sample.vcf.gz" },
"client_ref": "CASE-00123",
"options": { "genome_build": "GRCh37" }
}

You name the VCF itself, not a folder — nothing else in the prefix is listed or read, so a manifest, an index, or a second VCF beside it are all irrelevant, and the format of your own case metadata stays yours. Everything the analysis needs arrives as typed options. The key must end in .vcf, .vcf.gz or .vcf.bgz; uncompressed, bgzip- and plain gzip-compressed files are all accepted and you never need to recompress anything.

The annotated copy is written beside the input, named after it and after the job: sample.vcf.gz becomes sample.annotated.<job>.vcf.gz in the same prefix, where <job> is the first eight characters of the job_id. Read the exact name from data.output.uri rather than constructing it.

The job is in the name because the input alone does not identify a run: analysing one VCF against two different panels produces two different results, and under an input-only name the second would overwrite the first while the first receipt — retained for 24 hours — still pointed at it. Two VCFs in one folder are likewise two independent cases. A resubmission resolves to the same job and so rewrites the same object rather than accumulating copies.

Grant the service s3:GetObject on the object and s3:PutObject on its prefix. No s3:ListBucket is needed, because nothing searches.

  • input.type: s3_object today. The field is a discriminated union so another kind of input — an inline VCF, a presigned URL — can be added without breaking this one.
  • options.genome_build (required): GRCh37. A VCF header declares its assembly only sometimes, and guessing misplaces every window.
  • options.window: bp from a start site within which a variant is scored. Default 5000, range 1–100,000.
  • options.tissues: cell-type contexts, one scoring run each. Default ["heart", "liver", "brain"], 1–16 entries. The expression model is conditioned on these strings and the exact wording moves the result.
  • options.genes: restrict scoring to a panel. Accepts HGNC symbols (BRCA1) or Ensembl gene ids with or without a version (ENSG00000141510, ENSG00000141510.17); symbols are case-insensitive, up to 1,000 entries. Omit it to score every gene near a variant.
  • client_ref: an opaque string of yours, ≤128 characters, echoed back. It carries your case identity so this API never has to model a case.

The request is validated before the job is accepted. All of these are 400 bad_request on the POST, so a typo costs a second rather than a poll cycle through a job that fails minutes later: a URI that is not s3://bucket/key; one naming a prefix rather than an object; a key not ending .vcf, .vcf.gz or .vcf.bgz; an object this service cannot read (check the s3:GetObject grant); an empty VCF, or one over 2 GiB; a genes identifier matching no gene; a genome_build with no published start-site catalogue. Anything that goes wrong after acceptance is a job failure visible on the poll — including a file that turns out to be unreadable, or to already carry GEX_* fields, neither of which can be known until it is opened.

A panel is the difference between a long case and a short one. Unrestricted, a whole exome scores tens of thousands of allele-to-start-site pairs; restricted to a handful of genes it scores a handful. Records near a start site that is not in your panel are still written to the output, flagged NO_REQUESTED_GENE_IN_WINDOW — deliberately distinct from OUTSIDE_TSS_WINDOW, because "you did not ask about this gene" and "there was no start site here" are different answers, and a narrow panel should not read as a coverage gap. The resolved panel is echoed in meta.options_applied.genes and recorded in the output VCF's ##GEXRequestedGenes header, so a restricted run cannot later be mistaken for a whole-genome one. An identifier matching no MANE Select gene is a 400 naming every unresolved one — a panel silently missing a gene would return a result that looks complete and is not.

Submitting the same case twice gives you the same job. The job_id is derived from your key, input.uri, the input object's ETag, client_ref and options, so a retry after an ambiguous failure — or a restarted integration — resolves to the run you already have rather than starting a second hours-long pass over the same object. There is no idempotency header to send. Two consequences worth knowing: a failed job re-runs on resubmission, and to deliberately re-run a case that succeeded, change client_ref. Replacing the VCF under an unchanged path also yields a new job, since its ETag is part of the derivation.

Accepted submissions return 202 with data = {job_id, status: "accepted", links}, plus X-Job-Id and Content-Location. On completion the poll returns:

{
"data": {
"task": "genomic_variant_interpretation",
"client_ref": "CASE-00123",
"status": "ok",
"output": { "type": "s3_object", "uri": "s3://your-bucket/cases/CASE-00123/sample.annotated.3eaecf69.vcf.gz" },
"counts": {
"variants_in": 73771,
"annotated": 12721,
"not_run": 61050,
"not_run_reasons": { "OUTSIDE_TSS_WINDOW": 61050 },
"annotated_alleles": 12725,
"associations": 14698,
"skipped_alleles": 10
}
},
"meta": {
"task": "genomic_variant_interpretation",
"genome_build": "GRCh37",
"job_id": "…", "request_id": "…",
"input": { "type": "s3_object", "uri": "s3://your-bucket/cases/CASE-00123/sample.vcf.gz" },
"gene_annotation": {
"build": "v50lift37",
"md5": "bed31c2cead98e9e1989ef7943fb3206",
"transcripts": "MANE Select",
"start_sites": 19153
},
"options_applied": { "window": 5000, "tissues": ["heart", "liver", "brain"], "genes": null }
}
}

Every input record reaches the output, scored or not, so annotated + not_run == variants_in and you can diff input against output record for record. A record that could not be scored is flagged in place with GEX_NOT_RUN and a reason in GEX_NOT_RUN_REASONOUTSIDE_TSS_WINDOW, NO_TSS_ON_CONTIG, NO_SUPPORTED_ALT, or NO_REQUESTED_GENE_IN_WINDOW when you sent a panel — rather than dropped. The same four appear in counts.not_run_reasons, which sums to not_run. A record with some usable and some unusable ALT alleles — a symbolic allele beside a plain one, say — lists the unusable ones in GEX_SKIPPED_ALT and is still annotated for the rest, so a record can carry both keys. Scored records carry GEX_ANN, one VEP-style entry per supported ALT allele and nearby transcript: Allele|Gene|Gene_ID|Transcript_ID|TSS|Strand|TSS_distance| then one log2 fold-change field per requested tissue. Distances are strand-aware, negative upstream, and the output header records the convention along with the window and the GENCODE release the start sites came from — and the model, once one is named. meta.input echoes the object you named, so a stored receipt names both what was read and what was written. counts.associations is the unit of scoring work — allele-to-start-site pairs, not records — so it is the number to reason about when sizing a case, and meta.gene_annotation is the machine-readable form of that provenance.

meta.model tells you whether these are model output. While this workflow is under development the field is absent, as is the annotated VCF's ##GEXModel header line — the run is not attributed to a model that did not produce the values. Both appear, naming the model, once it is. Everything around them is real today: your file is read, every record is matched against GENCODE v50lift37 MANE Select start sites, every record is written back, and the counts are computed from it.

Submitting a VCF that already carries GEX_* fields — an annotated copy from a previous run, for instance — is a 400: submit the original. Results are retained for 24 hours and then return 410. The annotated file in your own bucket is not on that clock. A job does not survive a deployment of this service: in-flight work is lost and the poll reports 503 with a message to resubmit, so retries belong to you.

This workflow is REST-only — it is not exposed as an MCP tool.


Next: Limits for caps and latency, or the REST API guide for the call walkthrough.