chain()
Sequence nodes, each output feeding the next input. Chains are nodes, so they nest.
Sequencing#
chain(id, ...nodes) runs nodes one after another, feeding each one's output into the next one's input. The chain's input is the first node's input; its output is the last node's output. That's the whole contract.
import { chain, step, ask, noul } from "jevchain";
const pipeline = chain(
"moderate",
step("clean", (s: string) => s.trim()), // string → string
ask("read", { questions: { rude: noul("Is this rude?") } }), // string → { rude: NoulAnswer }
step("decide", (a) => (a.rude.noul > 0.7 ? "hide" : "show")), // → "hide" | "show"
);
// ChainNode<string, "hide" | "show">Remember the mental model: route and gate pass their input through untouched, so a chain step after a route receives whatever the taken branch produced.
Types across the links#
Every node carries phantom input and output types, and chain's overloads line them up: chain(a, b) only compiles when a's output fits b's input. Get it wrong and you find out in your editor, not in production at 3am:
const c = chain( "c", step("n", () => 1), step("s", (x: string) => x),);error TS2345: Argument of type 'StepNode<unknown, number>' is not assignable to parameter of type 'JevNode<unknown, string>'.
Types of property '[io]' are incompatible.
…
Types of property 'out' are incompatible.
Type 'number' is not assignable to type 'string'.The message is long but the last line is the one that matters. (The [io] property is the phantom field that carries the types. It's never set at runtime.)
To name a node's types, use InputOf and OutputOf. OutputOf is the one you'll reach for most: it types a step's parameter from the node before it, so the step follows along when you add a question.
import type { InputOf, OutputOf } from "jevchain";
type In = InputOf<typeof pipeline>; // string
type Out = OutputOf<typeof pipeline>; // "hide" | "show"
// Type a step's parameter from whatever feeds it, so it follows along when that changes.
const read = ask("read", { questions: { rude: noul("Is this rude?") } });
const decide = step("decide", (a: OutputOf<typeof read>) => a.rude.noul > 0.7);Nesting#
A chain is a node, so chains go anywhere nodes go: inside other chains, as route branches, as gate paths, as parallel branches, as a cascade fallback. Nesting is free. A chain has no vertex of its own in the graph; its steps are just laid end to end, and in the trace it's a span whose children are its steps (paths like $/moderate/0, $/moderate/1).
// Small chains, named for what they do…
const understand = chain("understand", clean, read);
const respond = chain("respond", decide, format);
// …composed into bigger ones. Same rules, same types.
export const moderate = chain("moderate", understand, respond);
// A chain can be a branch, too.
const desk = route("desk", {
ask: choice("Which queue?", ["moderation", "support"]),
branches: { moderation: moderate, support: supportFlow },
});Want a friendlier label in the studio and the graph? describe(node, { title, description }) returns a copy with UI metadata attached.
import { describe } from "jevchain";
// A copy with a title/description for UIs; the original is untouched.
const understand = describe(chain("understand", clean, read), {
title: "Understand the message",
description: "Normalize, then ask Jev what it is.",
});evaluate chain: one ask with two questions and a step typed with OutputOf. Try blaming Dave.const rateExcuse = ask("rate-excuse", {
questions: {
plausible: score("How plausible is this excuse for missing standup?", [
"my dog ate my laptop",
"suspicious",
"plausible",
"airtight",
]),
blamesSomeone: noul("Does the excuse blame a coworker?"),
},
});
// OutputOf pulls a node's output type out, so the next step is fully typed.
type Rating = OutputOf<typeof rateExcuse>;
const verdict = step("verdict", (r: Rating) =>
r.blamesSomeone.noul > 0.5
? "Excuse rejected. We don't throw Dave under the bus."
: r.plausible.score >= 2
? "Excused. See you tomorrow."
: "Noted. Your camera will be on next time.",
);
// A chain is a node, so chains nest inside chains.
const evaluate = chain("evaluate", rateExcuse, verdict);
const excuses = chain(
"excuse-evaluator",
step("clean", (s: string) => s.trim()),
evaluate,
);Sorry, I had a dentist appointment I forgot to put in the calendar.
Seven links, then nest#
The typed overloads cover one to seven nodes. That's not a runtime limit; it's where the type signatures stop. Past seven you'll get an overload error, and the fix is the one you'd want anyway:
- Group related links into named sub-chains. Seven anonymous steps in a row is a code smell with a trace.
- Named sub-chains show up as their own spans, so “which part was slow?” is answered by looking, not by adding logs.
- They're reusable: the same sub-chain can sit in two routes. Ids needn't be unique; paths are.