Serialization.
Chains are data. JSON in, JSON out, and TypeScript back out again.
A node is already a plain object: the same shape you'd write by hand in JSON, plus inline functions where you supplied code. So serializing is nearly the identity function, and one definition can drive your code, the studio and the diagrams on this site. Every block of JSON and TypeScript below was generated at build time from the real chains.
toJSON#
toJSON(chain, meta?) returns a ChainDocument. Questions, thresholds, templates, titles: all of it round-trips exactly. Here's the fridge route, in full:
const doc = toJSON(fridge, { name: "Fridge verdict" });{
"format": "jevchain/v1",
"name": "Fridge verdict",
"root": {
"kind": "route",
"id": "fridge-verdict",
"title": "What do we do with this leftover?",
"ask": {
"type": "choice",
"instructions": "What should happen to this leftover?",
"criteria": {
"eat": "still good, and honestly it'll be better today",
"freeze": "fine now, but won't survive the week",
"bin": "past saving: fuzzy, sour, or of unknown origin"
}
},
"lowConfidence": {
"below": 0.45,
"then": {
"kind": "emit",
"id": "smell-test",
"value": "Smell it. Report back."
}
},
"branches": {
"eat": {
"kind": "emit",
"id": "eat",
"value": "Eat the {{input.item}}. Tonight. No notes."
},
"freeze": {
"kind": "emit",
"id": "freeze",
"value": "Freeze the {{input.item}}. Future you says thanks."
},
"bin": {
"kind": "emit",
"id": "bin",
"value": "Bin the {{input.item}}. Do not open the lid first."
}
}
},
"refs": []
}refs is empty and it loads with no handlers at all.const fridge = route("fridge-verdict", {
title: "What do we do with this leftover?",
ask: choice("What should happen to this leftover?", {
eat: "still good, and honestly it'll be better today",
freeze: "fine now, but won't survive the week",
bin: "past saving: fuzzy, sour, or of unknown origin",
}),
// Jev's confidence is a second axis: under 0.45, don't guess.
lowConfidence: { below: 0.45, then: emit("Smell it. Report back.", { id: "smell-test" }) },
branches: {
eat: emit("Eat the {{input.item}}. Tonight. No notes.", { id: "eat" }),
freeze: emit("Freeze the {{input.item}}. Future you says thanks.", { id: "freeze" }),
bin: emit("Bin the {{input.item}}. Do not open the lid first.", { id: "bin" }),
},
});{"item":"curry","age":"1 day","notes":"covered, smells amazing"}
fromJSON and handlers#
Code can't be JSON, so functions become { "$ref": "name" } and the document lists every name it needs in refs. The name tag printer has two steps:
{
"format": "jevchain/v1",
"root": {
"kind": "chain",
"id": "name-tag-printer",
"steps": [
{
"kind": "step",
"id": "normalize",
"run": {
"$ref": "normalize"
}
},
{
"kind": "route",
"id": "greeting",
"ask": {
"type": "choice",
"instructions": "What energy does this person bring?",
"criteria": {
"chaotic": null,
"calm": null
}
},
"state": "{{input.bio}}",
"branches": {
"chaotic": {
"kind": "emit",
"id": "emit",
"value": "HELLO MY NAME IS {{input.name}} 🎉"
},
"calm": {
"kind": "emit",
"id": "emit",
"value": "hello, my name is {{input.name}}."
}
}
},
{
"kind": "step",
"id": "print",
"run": {
"$ref": "print"
}
}
]
},
"refs": [
"normalize",
"print"
]
}fromJSON(doc, options) (a document or a JSON string) puts the functions back from a handlers map, then validates the result with the same checks run uses.
import { fromJSON } from "jevchain";
const nameTag = fromJSON(doc, {
handlers: {
normalize: (raw: string) => ({ name: raw.trim().split(/\s+/)[0], bio: raw.trim() }),
print: (tag: string, ctx) => ({ tag, original: ctx.runInput }),
},
});
await jev.run(nameTag, "Harriet. Enjoys well-labelled spreadsheets.");| name | type | what it does |
|---|---|---|
| step run | ref ?? id | The step's ref option if you set one, else its id. Set ref when two steps share one function. |
| function state | <nodeId>.state | An ask, route or gate whose state is a function. Template strings stay strings and need nothing. |
| parallel join | <nodeId>.join | A parallel's join function. |
| tier state | <cascadeId>.<tierId>.state | A cascade tier with a function state. |
| name | type | default | what it does |
|---|---|---|---|
| handlers | Record<string, Handler> | A function for every name in refs. | |
| missingHandlers | "throw" | "passthrough" | default "throw" | throw: a ChainConfigError listing every missing name. passthrough: steps without a handler return their input and log a note, handy for previews. |
// Load a document without its code, e.g. to draw it or dry-run the decisions.
const preview = fromJSON(doc, { missingHandlers: "passthrough" });
// Each unbound step returns its input unchanged and logs:
// no handler bound for "normalize", passed input throughThe jevchain/v1 format#
One small envelope around the root node. It's the format the library, the studio and this site share.
| name | type | what it does |
|---|---|---|
| format | "jevchain/v1" | fromJSON refuses anything else. |
| name, description | string? | For humans and UIs. |
| examples | Json[]? | Sample inputs, for UIs and docs. |
| root | Json | The chain itself. Each node has kind and id, plus its kind's fields exactly as the builders take them. |
| refs | string[] | Every handler name the document needs, sorted. |
- Nodes nest where they do in code:
branches,then/otherwise,unsure.then,lowConfidence.then,tiersandfallback,steps. - Questions use TypeSafe's wire format unchanged:
type,instructions,criteria. A choice built from an array of labels hasnullcriteria.
toTypeScript#
toTypeScript(doc, options?) goes the other way: from a document back to builder code that reads like you wrote it. It's the studio's “export to code” button. The fridge document from above comes back as:
import { choice, emit, route } from "jevchain";
/** Fridge verdict */
export const fridgeVerdict = route("fridge-verdict", {
ask: choice("What should happen to this leftover?", {
eat: "still good, and honestly it'll be better today",
freeze: "fine now, but won't survive the week",
bin: "past saving: fuzzy, sour, or of unknown origin",
}),
branches: {
eat: emit("Eat the {{input.item}}. Tonight. No notes.", { id: "eat" }),
freeze: emit("Freeze the {{input.item}}. Future you says thanks.", { id: "freeze" }),
bin: emit("Bin the {{input.item}}. Do not open the lid first.", { id: "bin" }),
},
lowConfidence: { below: 0.45, then: emit("Smell it. Report back.", { id: "smell-test" }) },
title: "What do we do with this leftover?",
});Functions can't be recovered from a name, so each $ref becomes a clearly marked stub for you to fill in:
import { chain, choice, emit, route, step } from "jevchain";
export const nameTagPrinter = chain(
"name-tag-printer",
step("normalize", async (input: any, ctx) => {
// TODO: implement "normalize"
return input;
}),
route("greeting", {
ask: choice("What energy does this person bring?", ["chaotic", "calm"]),
branches: {
chaotic: emit("HELLO MY NAME IS {{input.name}} 🎉"),
calm: emit("hello, my name is {{input.name}}."),
},
state: "{{input.bio}}",
}),
step("print", async (input: any, ctx) => {
// TODO: implement "print"
return input;
}),
);| name | type | default | what it does |
|---|---|---|---|
| exportName | string | default camelCase(root id) | Name of the exported constant. |
| importFrom | string | default "jevchain" | Module the builders are imported from. |