Beta. This plugin is early and under active development. The function names, the shape of the answers and the
jevconfiguration scope may all change between releases, and there is no deprecation cycle yet. Pin a version, and expect to revisit pipelines that use it.
nf-jev exposes TypeSafe System One judgments as ordinary Nextflow
functions. A pipeline builds typed questions as values, asks them, and gates on the returned
probabilities with plain Nextflow operators.
Jev is not a language model. It generates no text, holds no conversation and calls no tools: it answers typed questions about the state you give it, and returns a probability distribution over the answers you allowed. That makes it something a pipeline can branch on — unlike a number a language model reports about its own confidence, which it invented.
Four functions, nothing else:
noul (instructions [, criteria]) // is this true? -> probability
choice (instructions, criteria) // which one? -> winner + distribution
score (instructions, levels) // how much? -> position + distribution
jev (state, questions) // answer them all against one state, in one request
See SPEC.md for the design, and what it deliberately leaves out.
Enable the plugin in your pipeline nextflow.config:
plugins {
id 'nf-jev@0.2.0'
}Set your credential — either jev.apiKey in the configuration, or the TYPESAFE_API_KEY
environment variable:
export TYPESAFE_API_KEY="..."Then ask a question:
include { jev; noul } from 'plugin/nf-jev'
workflow {
channel.of('The assembly meets the stated QC bar.')
.map { text -> jev(text, [clinical: noul('The text gives clinical guidance.')]) }
.view { answers -> "clinical: ${answers.clinical.noul}" }
}jev {
provider = 'typesafe' // or 'openrouter', see below
apiKey = secrets.TYPESAFE_API_KEY // or $TYPESAFE_API_KEY
model = 'jev-latest' // pin a snapshot to fix a result
endpoint = 'https://api.typesafe.ai/v1/systemone'
timeout = 30 // seconds, per request
cacheDir = "$projectDir/.jev-cache" // unset = no caching
}Everything but the key has a default. provider picks the defaults for endpoint and model and
names the environment variable the key falls back to; it is inferred from the host of endpoint
when left out, so an openrouter.ai endpoint alone is enough to select OpenRouter.
Jev is also served by the OpenRouter Decisions API. Select it with one setting:
jev {
provider = 'openrouter' // endpoint https://openrouter.ai/api/alpha/decisions
apiKey = env('OPENROUTER_API_KEY') // the default fallback; or secrets.OPENROUTER_API_KEY
model = 'typesafe/jev-1.13' // the default; OpenRouter ids are namespaced
}Only OPENROUTER_API_KEY is read for OpenRouter and only TYPESAFE_API_KEY for TypeSafe, so a
TypeSafe key is never presented to openrouter.ai because the OpenRouter one happens to be unset —
the run stops and names the variable that is missing. Naming one provider while endpoint
points at the other's host (say provider = 'typesafe' with an openrouter.ai endpoint) is
refused outright; a proxy on any other host is fine.
Call jev(state, questions) as usual: both APIs take the same request and return the same
answers. OpenRouter reports 502, 503 and 524 as transient on top of the 429 and 529 TypeSafe
uses; all five are retried with backoff. Its response adds id, provider and usage.cost,
which the plugin ignores, and its model field names the dated snapshot that answered, e.g.
typesafe/jev-1.13-20260917. That dated id can be sent as model to pin it — see
Caching.
A noul can spell out what a yes and a no mean with the two-argument form, on either provider:
noul('Has this run been seen before?',
[true: 'The title or accession mentions a prior submission', false: 'Nothing points to one'])Set jev.cacheDir and responses are cached, keyed on a SHA-256 of the exact request — model,
state and questions. Unset it and nothing is cached. Entries are published atomically, so
concurrent runs can share a directory; an entry that will not parse is treated as a miss and the
question re-asked, so a truncated file can never poison a pipeline. There is no eviction: rm -rf
the directory.
Two things to know before turning it on:
- Pin the model. With the default
model = 'jev-latest'the key holds a floating alias, so cached answers keep being replayed after the alias moves to a newer snapshot. The plugin warns whenever the model id does not name a fixed snapshot —jev-latest,jev-previewand OpenRouter'stypesafe/jev-1.13all float. PaircacheDirwithmodel = 'jev-1.13.0'on TypeSafe, or a dated id such asmodel = 'typesafe/jev-1.13-20260917'on OpenRouter. - A cache hit freezes one draw. Repeated live calls on identical input vary a little. Caching makes a run reproducible, not the judgment — a threshold sitting exactly on a boundary will stop flapping for the wrong reason.
Keep cacheDir on a local filesystem; the atomic-publish guarantee is weaker on NFS.
answers.is_human // [type: 'noul', noul: 0.98]
answers.assay // [type: 'choice', choice: 'RNA-seq', confidence: 0.99, probabilities: [...]]
answers.tissue // [type: 'score', score: 1.97, confidence: 0.96, legend: [...], probabilities: [...]]A noul has no separate confidence — the probability is the answer, and 0.5 means "equally
likely either way", not "moderately". On a choice or score, confidence says how concentrated
the distribution is, not whether the answer is right.
Questions sharing a state should be sent together: they are evaluated in parallel and cannot see one another's answers, so a whole rubric costs one request.
Three runnable pipelines under examples/, each against the live API. Every
example has its own README explaining what it asks and what the output means.
label-samples — fetch ENA run metadata with an ordinary process,
label each run against a controlled vocabulary, and split the cohort on a calibrated confidence:
nextflow run examples/label-samplesACCEPT SRR891268 Homo sapiens / ATAC-seq (p=1.0, cell line p=0.95)
ACCEPT SRR031708 Drosophila melanogaster / RNA-seq (p=1.0, cell line p=0.73)
Note SRR891268: its ENA library_strategy is the useless OTHER, and the assay is recovered
from the free-text title alone.
route — pick a QC depth per sample and dispatch to different processes. No
agent and no generated text; a low-confidence pick falls through to the more thorough branch,
which is one readable line rather than a hope expressed in a prompt.
guardrail — screen generated text with a hazard rubric (three nouls and
a severity score) in a single request, with the block/review/pass thresholds in nextflow.config:
REVIEW R1 clinical=0.01 overclaim=0.48 identifying=0.04 severity=1.37
BLOCK R2 clinical=0.96 overclaim=0.78 identifying=0.08 severity=2.95
BLOCK R3 clinical=0.12 overclaim=0.74 identifying=0.67 severity=2.40
Built from the Nextflow plugin template.
Copy .env.example to .env for local credentials — the Nextflow Registry token used by
make release, and the TYPESAFE_API_KEY the examples need. make sources it into the environment of the
command it runs, and .env is gitignored. The token is read from the environment, so it never
appears in a command line.
make assemble # build
make test # unit tests; no test makes a live API call
make install # install into the local Nextflow plugins dir
make release # publish to the Nextflow RegistryCI builds and runs the unit tests on every push and pull request. It then installs the plugin and
runs all three examples against the live API, asserting how many samples each one decided — never
the probabilities themselves, which move slightly between runs. That job needs a TYPESAFE_API_KEY
repository secret; without one it reports a notice and skips, and it does not run for pull requests
opened from a fork, which have no access to secrets.
Apache License 2.0. See the COPYING file for details.