skip to content
docs / running & inspecting

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:

save.ts
const doc = toJSON(fridge, { name: "Fridge verdict" });
fridge.chain.json
{
  "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": []
}
▶ run itFridge verdictdocs chainchains.ts ↗
The chain behind that document. No functions anywhere, so refs is empty and it loads with no handlers at all.
chains.ts
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" }),
  },
});
route · fridge-verdictrouteWhat do we do with this lef…emit · eatemiteatemit · freezeemitfreezeemit · binemitbinemit · smell-testemitsmell-testeatfreezebinunsure

{"item":"curry","age":"1 day","notes":"covered, smells amazing"}

open in studio →

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:

name-tag.chain.json
{
  "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.

load.ts
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.");
where $refs come from, and the handler key to bind
step runref ?? idThe step's ref option if you set one, else its id. Set ref when two steps share one function.
function state<nodeId>.stateAn ask, route or gate whose state is a function. Template strings stay strings and need nothing.
parallel join<nodeId>.joinA parallel's join function.
tier state<cascadeId>.<tierId>.stateA cascade tier with a function state.
fromJSON options
handlersRecord<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.
preview.ts
// 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 through

The jevchain/v1 format#

One small envelope around the root node. It's the format the library, the studio and this site share.

ChainDocument
format"jevchain/v1"fromJSON refuses anything else.
name, descriptionstring?For humans and UIs.
examplesJson[]?Sample inputs, for UIs and docs.
rootJsonThe chain itself. Each node has kind and id, plus its kind's fields exactly as the builders take them.
refsstring[]Every handler name the document needs, sorted.
  • Nodes nest where they do in code: branches, then/otherwise, unsure.then, lowConfidence.then, tiers and fallback, steps.
  • Questions use TypeSafe's wire format unchanged: type, instructions, criteria. A choice built from an array of labels has null criteria.

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:

fridge.ts
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:

name-tag.ts
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;
  }),
);
toTypeScript options
exportNamestringdefault camelCase(root id)Name of the exported constant.
importFromstringdefault "jevchain"Module the builders are imported from.

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.