effect-questions
September 17, 2026 ยท View on GitHub
Semantic judgment as an Effect primitive.
const triage = Effect.gen(function*() {
const q = Questions.about(ticket);
if (yield* q.is("Is production work blocked?")) {
yield* prioritize(ticket);
}
return yield* q.branch("What kind of help is needed?", {
"Invoices, payments, or refunds": () => handleBilling(ticket),
"Bugs, outages, or deployment failures": () => investigate(ticket),
"Account access or membership": () => handleAccount(ticket),
}, { confidence: 0.6 });
});
Only the chosen handler runs. Its result, errors, and services flow through the returned Effect.
If the model is not confident enough, the Effect fails with UncertainDecision. Catch it where
the fallback belongs.
Operations
Questions.about(state) gives you:
is: a yes/no answer.ask: several questions in one request. Strings are yes/no.Question.choiceandQuestion.scoreadd options and rubrics.choose: pick one of your own objects. You get the object back.rank: every candidate, best first, with its probability.branch: pick a handler by its description and run only that one.probability,score,evidence: the numbers behind an answer.
Two more shapes. Questions.is(question) is a predicate over context. Questions.each(items)
binds a whole collection and answers for every item in one request. To combine answers, ask a
batch and use &&, ||, and ! in an Effect.map.
const transient = Questions.is("Does this error describe a temporary failure?");
const actionable = (issue: string) =>
Questions.about(issue).ask({
bug: "Is this a bug report?",
reproducible: "Does it include steps to reproduce?",
security: "Does it describe a security issue?",
}).pipe(Effect.map(({ bug, reproducible, security }) => (bug && reproducible) || security));
const program = Effect.gen(function*() {
const report = yield* fetchReport.pipe(Effect.retry({ while: transient, times: 3 }));
const worthReading = yield* Effect.filter(issues, actionable, { concurrency: 4 });
const urgency = yield* Questions.each(titles).score("How urgent is this?", [
"Low",
"Medium",
"High",
]);
});
Live context
The context can be an Effect. It is read every time a question runs.
const program = Effect.gen(function*() {
const facts = yield* Ref.make({ report, findings: [] });
const settled = Questions.about(Ref.get(facts)).is("Do the findings establish the cause?");
yield* probeOnce.pipe(Effect.repeat({ until: () => settled, times: 3 }));
});
settled is one value. Each time it runs, it reads the current facts and asks again. That is a
do-while loop with a model in the condition, and nothing new to learn.
When the next step is a choice, offer "stop" as one of the candidates. One request then answers both "what next" and "are we done".
Modules
Questions: the API above.Question:choice,score, andbooleandefinitions. Option keys are the answers.Answer: rank, aggregate, and take expected values over probabilities.Decision: confidence gates, expected loss, and dispatch over evidence.QuestionModel: the provider service, plus token metrics and a span per evaluation. Jev is the first provider.
Built on Effect v4 and TypeScript 7.
Run
Needs Node 24 and pnpm. Live examples read TYPESAFE_API_KEY.
pnpm install
node examples/triage.ts
node examples/investigation.ts
All examples are described in examples/README.md.
Development: pnpm check and pnpm build.