skip to content
docs / building blocks

ask()

One Jev call, any number of questions about the same input. Outputs the typed answers.

One call, many questions#

ask sends its questions about the node's input to Jev in one request and outputs the answers, keyed like the questions. Jev reads the state once and answers every question in parallel, so five questions cost roughly what one does. Ask for everything you need up front.

read.ts
import { ask, choice, noul, score } from "jevchain";

const read = ask("read-the-pr", {
  title: "Read the PR",
  questions: {
    clarity: score("How clearly does the description explain the change?", [
      "no description", "vague", "clear", "exemplary",
    ]),
    tests: noul("Does the PR add or update tests?"),
    scope: choice("What kind of change is this?", ["typo", "feature", "refactor", "migration"]),
  },
});

The output is a plain object of answers, one per key:

{
  "clarity": { "type": "score", "score": 1.9, "probabilities": { "0": 0.02, "1": 0.2, "2": 0.64, "3": 0.14 },
               "legend": { "0": "no description", ... }, "confidence": 0.31 },
  "tests":   { "type": "noul", "noul": 0.93 },
  "scope":   { "type": "choice", "choice": "feature",
               "probabilities": { "typo": 0.01, "feature": 0.88, "refactor": 0.08, "migration": 0.03 },
               "confidence": 0.62 }
}
ask(id, config)
idstringNames the node in traces and graphs. Needn't be unique.
questionsQuestionsNamed questions from choice, score and noul. At least one.
statestring | (input) => Entrydefault the inputWhat Jev reads. See below.
modelstringdefault client's modelPin this node to a model, e.g. "jev-1.13.0".
title / descriptionstringLabels for UIs and traces.

Choosing the state#

By default Jev sees the node's input. Often you want it to see less (just the message, not the metadata), or something reshaped. state takes one of three forms:

state.ts
// 1. omitted: the node's input, as-is (strings stay strings, objects go as JSON)
ask("a", { questions });

// 2. a template: one hole keeps the raw value, so objects stay structured
ask("b", { questions, state: "{{input.messages}}" });

// 2b. text with holes: values are stringified into the text
ask("c", { questions, state: "From {{input.user}}: {{input.text}}" });

// 3. a function: full control (serialized as a $ref)
ask("d", { questions, state: (pr: PullRequest) => ({ title: pr.title, body: pr.body }) });
  • Templates are paths only: {{input.user.name}}, {{input.items.0}}. No expressions, no eval. Besides input you can reach run (the run's original input) and results (finished nodes' outputs, by id).
  • Whatever you end up with is coerced into something Jev accepts: null and undefined become null, numbers and booleans become strings, objects are made JSON-safe.

Typed output#

The output type is Answers<Q>: each key maps to the answer type of its question, and choice labels stay literal unions. Pull it out with OutputOf and the next step is checked end to end.

review.ts
import { chain, step, type OutputOf } from "jevchain";

type Read = OutputOf<typeof read>;

const verdict = step("verdict", (a: Read) =>
  a.scope.choice === "migration" && a.tests.noul < 0.5
    ? "no tests on a migration. bold."
    : `clarity ${a.clarity.score.toFixed(1)} / 3`,
);

export const review = chain("review", read, verdict);

Rename a label in the choice and the comparison in verdict stops compiling. That's the idea.

▶ run itPull Request Horoscopegallerysource →
Five questions about one PR in a single ask, then a step that weighs them into a risk number. a.scope.choice indexes a lookup table by label, and the compiler knows every label exists.
ask · read-the-praskRead the PRstep · horoscopestepCast the horoscope

{"title":"quick fix for users table","description":"drops the legacy_email column, should be fine. deploying friday evening so it's quiet","filesChanged":1}

open in studio →

When to reach for it#

ask is for when you want the numbers, not a branch. Rule of thumb:

  • You'll combine several answers in code (weights, lookup tables, a formula): use ask and a step.
  • One choice picks what happens next: use route. It asks and branches in one node, and the trace records the decision.
  • One number has to clear a bar: use gate.
  • Independent reads that each deserve their own node (or their own state): several asks under a parallel. Same state still means one request.

ask vs. alsoAsk#

Routes and gates take alsoAsk: extra questions that ride in the same call and are recorded in the trace but don't affect the branch. Use it for “I'll want to know this later” signals like sentiment. When you need to use the answers downstream, use an ask.

▶ run itGroup Chat Drama Triagegallerysource →
Four single-question asks under a parallel, all over the same chat. Same state, same tick: the client sends one request.
parallel · read-the-roomRead the roomjoin · read-the-roomcollectask · heataskheatask · passive-aggressionaskpassive-aggressionask · about-measkabout-meask · topicasktopicstep · make-a-planstepMake a planheatpassiveaboutMetopic

{"me":"Sam","messages":[{"from":"Priya","text":"so are we still doing brunch sunday"},{"from":"Sam","text":"can't this week sorry!!"},{"from":"Jordan","text":"classic"},{"from":"Priya","text":"no worries. we'll just pla…

open in studio →

api key

bring your own typesafe key, or ride the shared one (rate-limited, be nice).

shared key
checking…
your key
not set

your key stays in this browser (localStorage, jevchain.byok). it only travels to this site's /api/jev proxy in an x-typesafe-key header, which forwards it to typesafe and immediately forgets it. nothing is logged or stored server-side. requests on your own key get a much roomier rate limit.

keyboard shortcuts

fewer clicks, more chains. these work anywhere outside a text field.