# Tasks

Six tasks, each its **own published operation** — `POST /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](#find-genes-and-predict-expression) 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](mcp.md).

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](https://api.genomicintelligence.ai/redoc); per-task length caps and latency are in [Limits](reference/limits.md).

| Task | Input (bp) | Model window | Strand-sensitive | Output |
|---|---|---|---|---|
| [Promoter](#promoter) | 300–500,000 | 2,000 (300 on the `*-300bp` models) | Yes (coding strand) | Promoter-region probabilities |
| [Splice](#splice) | 100–500,000 | 15,000 | Yes (transcript orientation) | Donor / acceptor sites |
| [Enhancer](#enhancer) | 50–500,000 | 249 | Yes (dev/hk channels) | `dev` + `hk` activity scores |
| [Chromatin](#chromatin) | 200–500,000 | 1,000 | No | 919 features in 8 track groups |
| [Expression](#expression) | 9,198–500,000 (+ `tss_index`) | — (`trained_window_bp` 9,198) | Yes (coding strand) | Expression in log(TPM+1) |
| [Annotation](#annotation) | 1,000–500,000 | — | Plus-strand input | Transcripts with type + exon/intron/CDS structure (GFF3) |
| [Find genes + expression](#find-genes-and-predict-expression) | 1,000–500,000 | — | Plus-strand input | Genes found, each with a predicted expression |
| [Variant interpretation](#genomic-variant-interpretation) | A VCF, not a sequence | — | Coordinates, not strand | An 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](reference/limits.md#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 body** — *not* `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](reference/limits.md#the-floor-is-admission-control-not-regime). 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`](https://api.genomicintelligence.ai/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:

| Task | `options` keys | Required |
|---|---|---|
| Promoter | `threshold` (0–1, default 0.5) | — |
| Splice | `threshold` (0–1, default 0.5), `site_types` (subset of `["donor","acceptor"]`, default both) | — |
| Enhancer | *none — the object accepts no fields* | — |
| Chromatin | `threshold` (0–1, default 0.5) | — |
| Annotation | `batch_size` (1–128, default 8), `shift_coordinates` (string, e.g. `"UCSC"`), `reverse_complement` (bool, default `true`) | — |
| Expression | `description` (string) | **`description`**, and `options` itself |
| Find genes + expression | `description`, `annotation_model`, `expression_model`, `batch_size` (1–128, default 8), `shift_coordinates` | `options` itself; `description` enforced at runtime |

## The tasks, one card each

Each task below has its own notes, an example call from the [kit client](https://docs.genomicintelligence.ai/integration-kit/client) and from an [MCP](mcp.md) host, then **Applications** and **Usage tips**. On the rendered page ([docs.genomicintelligence.ai/tasks](https://docs.genomicintelligence.ai/tasks)) each card also carries Input, Options and Output panels, generated at build time from the pinned [`/v1/openapi.json`](https://api.genomicintelligence.ai/v1/openapi.json) so they cannot drift from it; **Source** on a panel opens that operation in ReDoc. The prose does not restate those schemas; outside the panels, read them in [ReDoc](https://api.genomicintelligence.ai/redoc) or in `/v1/openapi.json` itself.

## 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](rest-api.md#output-formats)).

### API reference

**Python client**

```python
import os
from gi_client import Client  # the kit's single-file client

client = Client(api_key=os.environ["GI_API_KEY"])  # sent as Authorization: Bearer gi_...
r = client.predict("promoter", sequence=seq, options={"threshold": 0.5})
regions = r["data"]["regions"]
```

**MCP**

Ask the [MCP server](mcp.md): *"Fetch a 100 kb window around human TP53 and scan it for promoter regions."*

### Applications

Use it to locate transcription start regions in unannotated sequence, or to score designed constructs for promoter strength inside an optimisation loop. The per-window detail in `data.window_details` makes it usable as a fitness signal rather than only a binary call.

### Usage tips

- **The floor is 300 bp** — the `minLength` on this operation's request schema. It is a property of the endpoint, so choosing a different `model` does not lower it.
- **Input shorter than the model's window is padded, not rejected.** A 300 bp submission is scored against `g0-promoter-2000bp`'s 2,000 bp window; compare your length with `bio_spec.context_window_bp` from `GET /v1/tasks/promoter/models` before reading much into a marginal call.
- **Submit the coding (sense) strand.** The task is strand-sensitive.

## 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](reference/limits.md#response-size-the-splice-site-cap).
- `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](rest-api.md#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.

### API reference

**Python client**

```python
# client = Client(api_key=os.environ["GI_API_KEY"]), as in Promoter
r = client.predict("splice", sequence=seq, options={"threshold": 1e-3, "site_types": ["donor", "acceptor"]})
sites = r["data"]["sites"]  # start/end bound a token, not the junction base
```

**MCP**

Ask the [MCP server](mcp.md): *"Fetch the human HBB gene sequence and predict its splice sites."*

### Applications

Use it to locate exon boundaries in unannotated transcripts, to check whether a designed edit creates or destroys a splice site, or to screen variants for splice disruption by scoring the reference and the edited sequence side by side.

### Usage tips

- **Submit the transcript's orientation.** The wrong strand does not fail loudly: it returns sites at different positions, often at high confidence, and no score or count identifies a mis-oriented submission afterwards. Reverse-complement minus-strand genes before you call.
- **A site's `start`/`end` is a token span, not the junction base** — typically 4–8 bp. Locate a boundary to within the span; do not reduce it to one base or intersect it with base-resolution annotation as though it were one.
- **The floor is 100 bp, the window 15,000 bp.** Treat 15,000 bp as the in-regime size.
- **Do not send `threshold: 0`.** A response over 20,000 sites is refused with `422 validation_failed`; `1e-3` returns effectively everything and stays under the cap.

## 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](rest-api.md#output-formats)).

### API reference

**Python client**

```python
# client = Client(api_key=os.environ["GI_API_KEY"]), as in Promoter
r = client.predict("enhancer", sequence=fly_seq)  # no options: EnhancerOptions accepts none
windows = r["data"]["windows"]
```

**MCP**

Ask the [MCP server](mcp.md): *"Fetch the Drosophila ftz gene and predict enhancer activity."*

### Applications

Use it to rank candidate enhancers, or as a dual-objective fitness function when designing regulatory elements that favour one programme (developmental or housekeeping) over the other.

### Usage tips

- **The floor is 50 bp**, the lowest of the six, but the model scores 249 bp windows. Submit at least 249 bp to stay in regime.
- **The developmental/housekeeping split is a *Drosophila* STARR-seq definition** (DeepSTARR). Read the two scores as relative activity within a comparison, not as calibrated cross-species values.
- **Send no options.** `EnhancerOptions` accepts no fields: send `{}` or omit it; any key is a `422`.

## 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](rest-api.md#output-formats)).

### API reference

**Python client**

```python
# client = Client(api_key=os.environ["GI_API_KEY"]), as in Promoter
r = client.predict("chromatin", sequence=seq, options={"threshold": 0.5})
windows = r["data"]["windows"]
print(r["meta"]["task_specific_counts"]["total_annotations"])
```

**MCP**

Ask the [MCP server](mcp.md): *"Fetch the human HBB promoter region and predict its chromatin features."*

### Applications

Use it to ask whether a sequence looks accessible, bound, or marked, and to compare designed variants against a natural reference across many assays at once.

### Usage tips

- **The floor is 200 bp; the window is 1,000 bp.** Submit at least 1,000 bp to avoid padding.
- **Lowering `threshold` grows the response.** Only features scoring above it are returned, and each window is scored on all 919.
- **A count is not a cell type.** `meta.task_specific_counts.total_annotations` sums calls across every window and every feature; a high count is not evidence about any one cellular context. Read the individual features for that.

## 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](reference/limits.md#expression-the-9198-bp-window-and-tss_index) 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.)

### API reference

**Python client**

```python
# client = Client(api_key=os.environ["GI_API_KEY"]), as in Promoter
r = client.predict(
    "expression",
    sequence=locus,            # >= 9,198 bp, gene-sense strand
    tss_index=tss,             # 0-based, into the whitespace-stripped sequence
    options={"description": "K562 cells"},
)
print(r["data"]["prediction"]["expression"])                  # log(TPM+1)
print(r["meta"]["task_specific_counts"]["scored_window"])     # assert on this
```

**MCP**

Ask the [MCP server](mcp.md): *"Predict HBB expression in K562 cells."*

### Applications

Use it to estimate the transcriptional output of a locus or a designed promoter under a stated cellular context, and as the objective in an expression-maximising or expression-matching design loop.

### Usage tips

- **The window is exact.** Submit exactly 9,198 bp centred on the TSS, or a longer locus plus `tss_index` and let the service cut it. Under-length input is rejected, never padded.
- **`description` is conditioning text, not a label.** It is fed to the model, so rewording it changes the prediction. Hold it fixed across runs you intend to compare.
- **An in-range but wrong `tss_index` still scores** — a different window, with a confident `200`. Read the applied window back from `meta.task_specific_counts.scored_window` rather than assuming the request's.
- **The sequence is never reverse-complemented.** Submit minus-strand genes in transcript orientation.

## 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](rest-api.md#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](rest-api.md#output-formats).

### API reference

**Python client**

```python
# client = Client(api_key=os.environ["GI_API_KEY"]), as in Promoter
# Prefer: respond-async, then poll GET /v1/tasks/jobs/{job_id}
job_id = client.submit_async("annotation", sequence=region, options={"shift_coordinates": "UCSC"},
                             sequence_name="chr8:127,680,000-127,800,000")
result = client.wait_for_job(job_id)
transcripts = result["data"]["transcripts"]
```

**MCP**

Ask the [MCP server](mcp.md): *"Find the genes in chr8:127,680,000-127,800,000."*

### Applications

Use it to annotate assembled contigs or synthetic constructs where no reference annotation exists, and to supply TSS positions (`tss_position`) to [expression](#expression) when they are not known in advance.

### Usage tips

- **The floor is 1,000 bp**, the highest of the five non-expression tasks.
- **One submission finds both strands.** Submit the plus-strand region; each transcript carries its own `strand`, relative to the sequence as submitted.
- **This is the slowest task, and the only predict operation with a sync cap.** Above 200,000 bp a synchronous JSON request is `413 sync_too_large`; send `Prefer: respond-async` and poll. Below the cap async is a latency preference, not a requirement.
- **`reverse_complement: false` is a faster single pass**, at some cost in minus-strand recall.

## 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](rest-api.md#asynchronous-jobs)). This is the lower of the contract's two sync caps; [annotation](#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.

### API reference

**Python client**

```python
# client = Client(api_key=os.environ["GI_API_KEY"]), as in Promoter
accepted = client.find_genes_and_predict_expression(region, description="K562 cells", async_=True)
result = client.wait_for_job(accepted["data"]["job_id"])
for gene in result["data"]["expression_predictions"]:
    print(gene.get("skip_reason") or gene["expression"])
```

**MCP**

Ask the [MCP server](mcp.md): *"Find the genes in chr8:127,680,000-127,800,000 and predict each one's expression in K562."*

### Applications

Use it when the TSS positions are not known in advance: it annotates and scores a whole locus in one call, rather than you chaining [annotation](#annotation) into [expression](#expression) yourself.

### Usage tips

- **The floor is 1,000 bp** (annotation's), and the ceiling is this endpoint's own 500,000 bp, which is not the expression model's window.
- **Above 50,000 bp, go async.** A synchronous request is `413 sync_too_large`; resend the identical body with `Prefer: respond-async`. This is the lower of the contract's two sync caps.
- **A gene near the edge of your region is still scored:** its window is padded with `N` rather than the gene dropped. A gene the workflow could not score carries `skip_reason`, and `genes_predicted + genes_skipped == genes_found`.
- **JSON only.** Unlike the predict operations it has no `?format=` parameter.

## 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.

> 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](resources/support.md) 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](rest-api.md#asynchronous-jobs)).

```http
POST /v1/workflows/genomic-variant-interpretation
Authorization: Bearer gi_...
Prefer: respond-async
Content-Type: application/json
```

```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:

```json
{
  "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_REASON` — `OUTSIDE_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](mcp.md) tool.

### API reference

**Python client**

The kit client does not wrap this workflow. Post it with `requests` and poll with the client:

```python
import os, requests
from gi_client import Client

key = os.environ["GI_API_KEY"]
r = requests.post(
    "https://api.genomicintelligence.ai/v1/workflows/genomic-variant-interpretation",
    headers={"Authorization": f"Bearer {key}", "Prefer": "respond-async"},
    json={
        "input": {"type": "s3_object", "uri": "s3://your-bucket/cases/CASE-00123/sample.vcf.gz"},
        "client_ref": "CASE-00123",
        "options": {"genome_build": "GRCh37"},
    },
    timeout=60,
)
receipt = Client(api_key=key).wait_for_job(r.json()["data"]["job_id"], max_wait=6 * 3600)
print(receipt["data"]["output"]["uri"])
```

**MCP**

This workflow is REST-only; it is not exposed as an [MCP](mcp.md) tool.

### Applications

Use it to score the variants in a case's VCF that fall near a transcription start site, across the tissues you name, and get the annotated VCF back in your own bucket beside the input, where the rest of your pipeline already reads.

### Usage tips

- **Async only.** `Prefer: respond-async` is required; without it the request is `400 bad_request`.
- **Send a gene panel when you have one.** Restricted to a handful of genes a case scores a handful of allele-to-start-site pairs instead of tens of thousands.
- **The same case twice is the same job.** To deliberately re-run a case that succeeded, change `client_ref`.
- **`tissues` are conditioning text.** The exact wording moves the prediction, as with expression's `description`.
- **Not results yet.** Until the response carries `meta.model` and the VCF a `##GEXModel` header, treat the values as format rather than findings.

---

Next: [Limits](reference/limits.md) for caps and latency, or the [REST API guide](rest-api.md) for the call walkthrough.
