jev / devtoolsDocumentation

API reference

Flow builder

Define requests, tasks, branches, parallel work, collections, and loops.

New to flows? Start with Flows from scratch, which introduces each method with a small example. For exact inputs and return values, use the API glossary.

defineJevFlow(name, build)#

Import from jev-devtools/flow in Node code. defineJevFlow<Input>() accepts a name and a synchronous builder callback, and returns a definition with run(input, options). The callback returns flow.output() or a terminal flow.branch().

ts · API help
import { choice } from "@typesafe-ai/sdk";
import { defineJevFlow } from "jev-devtools/flow";
import { inspector, jev } from "./server.js";

export const triage = <{ message: string }>("triage", (flow) => {
  const classification = flow.("classify", {
    : flow.,
    : {
      team: choice("Which team can help?", {
        billing: "Charges and payments",
        other: "Everything else",
      }),
    },
  });

  return flow.("route", {
    : classification.("team")..("billing"),
    : (path) => path.("billing-team"),
    : (path) => path.("support-team"),
  });
});

export async function routeMessage(message: string) {
  return triage.({ message }, { : jev, : inspector });
}

API reference: defineJevFlow() · flow.jev() · flow.input · flow.branch() · answer.choice · expression.eq() · flow.output() · program.run()

Example walkthrough: the classify Jev step asks which team should handle the message. .answer("team").choice.eq("billing") turns that answer into a branch condition. Each path returns a terminal team name. routeMessage() executes the recipe with the instrumented client and shared inspector.

Definition and run interfaces#

Value / member Type Description
triage.definition FlowDefinition The static recipe graph.
triage.run(input, options) Promise<FlowOutcome & { runId: string }> Executes the recipe and resolves to the fields below.
runId string Unique execution ID.
outcome string "billing-team" or "support-team" for this example.
value? unknown Optional output data; omitted by this example.

run accepts a required client with systemOne, optional devtools: FlowObserver, and optional signal: AbortSignal. It resolves to { runId, outcome, value? }. definition exposes the static graph for inspection. Definitions make no SDK calls.

Requests and function steps#

Method Arguments and result
jev(id, request, display?) A regular SDK request whose state can contain expressions. Returns .answer(name) field references and .answers.
jev(id, { input, request }, display?) Resolve input, then synchronously construct an SDK request in the factory. Use a preceding task for async work.
task(id, { input, run, timeoutMs? }, display?) Resolve nested input references and execute application code. Returns a typed expression for the result.

run on a task receives (resolvedInput, context). TaskContext contains signal, runId, stepId, and executionId. Respond to the signal for cooperative cancellation. Timeouts cannot undo side effects or forcibly stop non-cooperating code. Tasks do not retry automatically.

display is optional static { title?, description? } metadata. Keep it public-safe. Use .answers when question keys are generated dynamically; individual .answer(name) references require the named question to exist at execution.

Return interfaces#

Method / member Type Description
jev(...).answer(name) Object of typed field expressions References to the selected question's SDK result fields.
jev(...).answers FlowExpression of the answer object Reference to all answers. Both Jev overloads return this interface.
task(...) FlowExpression<Awaited<O>> Reference to the application callback's awaited result.

Decisions and outcomes#

Method Behavior
branch(id, { when, then, else }, display?) Terminal decision. Both paths must return output() or another terminal branch.
if(id, { when, then, else }, display?) Return a typed value from one path, then continue shared work.
output(name, value?, display?) Finish with a named outcome and optional resolved value. Does not execute a business action.

Path callbacks build both possible paths synchronously; only the selected path executes. Conditions use inspectable expressions. You cannot use a JavaScript if on an expression to select runtime work.

Return interfaces#

Method Type Description
branch(...) FlowExpression<FlowOutcome> Terminal result of the selected path.
if(...) FlowExpression<A | B> Selected value, available to shared steps after the join.
output(...) FlowExpression<FlowOutcome> Terminal outcome name and optional resolved value.

Parallel lanes#

parallel(id, lanes, display?) takes a named object of lane callbacks. Each lane returns an expression. The result is an object of joined expressions with matching names. Work inside a lane is sequential; lanes are concurrent. A sibling lane's private intermediate references are not available to other lanes.

ts · API help
const results = flow.("load", {
  left: (lane) => lane.("left-value", { : 2, : (n) => n * 2 }),
  right: (lane) => lane.("right-value", { : 3, : (n) => n * 3 }),
});
return flow.("joined", { left: results.left, right: results.right });

API reference: flow.parallel() · flow.task() · flow.output()

Example walkthrough: the left lane doubles 2 and the right lane triples 3. The joined references are passed into output(), producing { left: 4, right: 9 }. The lane computations are independent; neither reads the other's private steps.

Returned member Type Resolved value in this example
results.left FlowExpression<number> 4.
results.right FlowExpression<number> 9.

This fragment belongs inside a defineJevFlow() callback. For a complete request-based example, see the full flow guide.

Collections and loops#

map(id, { items, input, maxItems, concurrency? }, build, display?) executes one bounded item program per item and returns results in input order. Its builder input is { item, index, context }. The input option supplies explicit shared context.

loop(id, { initial, input, while, maxIterations }, build, display?) tests the state condition before each iteration. Its builder input is { state, index, context }; return the next state. It returns the final state after the condition becomes false. If the condition still holds at the iteration limit, it throws FlowLoopLimitError.

These complete definitions demonstrate application-owned tasks without inference:

ts · API help
import { defineJevFlow, all, not } from "jev-devtools/flow";

export const summarize = <{ values: number[] }>(
  "summarize",
  (flow) => {
    const doubled = flow.(
      "double",
      {
        : flow..("values"),
        : {},
        : 20,
        : 2,
      },
      (item) =>
        item.("multiply", {
          : item..("item"),
          : (value) => value * 2,
        }),
    );

    const total = flow.("total", {
      : doubled,
      : (values) => values.reduce((sum, value) => sum + value, 0),
    });

    const label = flow.("label", {
      : (total.(10), (total.(20))),
      : (path) =>
        path.("large", { : total, : (n) => `Large: ${n}` }),
      : (path) =>
        path.("regular", { : total, : (n) => `Regular: ${n}` }),
    });
    return flow.("summary", { total, label });
  },
);

export const count = <{ target: number }>("count", (flow) => {
  const initial = flow.("initial", {
    : flow..("target"),
    : (target) => ({ value: 0, target }),
  });
  const finalState = flow.(
    "advance",
    {
      ,
      : {},
      : (state) => state.("value").(state.("target")),
      : 10,
    },
    (iteration) =>
      iteration.("increment", {
        : iteration..("state"),
        : (state) => ({ ...state, value: state.value + 1 }),
      }),
  );
  return flow.("counted", finalState);
});

API reference: defineJevFlow() · flow.map() · flow.input · expression.get() · flow.task() · flow.if() · all() · expression.gt() · not() · expression.eq() · flow.output() · flow.loop() · expression.lt()

Example walkthrough: summarize uses map() to double each number, a task to add them, and if() to choose a label before returning the summary. all() and not() make the label policy explicit. With [2, 4], the output is { total: 12, label: "Large: 12" }. The separate count flow carries { value, target } as loop state and increments value until it reaches the target.

Method Return type Resolved value
map(...) FlowExpression<Output[]> Item results in input order.
loop(...) FlowExpression<State> Final state when its pre-iteration condition becomes false.

A loop can contain a map. Nested maps, nested loops, and loops inside maps are unsupported. Shared context is explicit; use context inside map/loop item programs rather than reaching across scope boundaries.

Limits and errors#

IDs must be unique across the whole definition, including untaken branches and lanes: 1–80 letters, digits, _, or -, starting with a letter or digit. Output names are node IDs too. Definitions allow 256 nodes, 16 nested blocks, and a run-wide budget of 1,024 expanded nodes.

Structural errors fail during definition. Missing input values and invalid comparisons fail when execution reaches them. FlowLoopLimitError is exported for bounded-loop handling. For exact collection/concurrency/timeout limits, execution errors, and cancellation behavior, consult the detailed guide and installed declarations.

Replay captured answers (unreleased)#

replayClient(fixture) returns an SDK-compatible client for a flow run. Jev steps receive stored answers without a network call or API key. This is not in npm 0.4.1 yet.

ts · API help
// Unreleased: use the working-tree package, not npm 0.4.1.
import { choice } from "@typesafe-ai/sdk";
import {
  defineJevFlow,
  replayClient,
  type FlowFixture,
} from "jev-devtools/flow";

export async function replayExample() {
  const flow = <{ message: string }>("saved-triage", (flow) => {
    const result = flow.("classify", {
      : flow.,
      : {
        team: choice("Which team?", { billing: null, other: null }),
      },
    });
    return flow.("routed", result.("team").);
  });

  // A hand-written fixture; Copy fixture can produce this shape from a run.
  const fixture:  = {
    : 1,
    : "saved-triage",
    : {
      classify: {
        model: "jev-sample",
        answers: {
          team: {
            type: "choice",
            choice: "billing",
            probabilities: { billing: 0.8, other: 0.2 },
            confidence: 0.6,
          },
        },
      },
    },
  };
  return flow.(
    { message: "I was charged twice." },
    { : (fixture) },
  );
  // outcome: "routed", value: "billing". No key or inference call.
}

API reference: defineJevFlow() · flow.jev() · flow.input · flow.output() · answer.choice · FlowFixture · program.run() · replayClient()

Example walkthrough: the fixture stores the response for the classify step. replayClient() supplies that response to the same flow, so the Choice expression resolves to "billing" without inference. The run returns outcome "routed" and value "billing".

API Return type Description
replayClient(fixture) TypeSafeClient Client that supplies saved answers inside a flow run.
replayExample() Promise of a flow run result Resolves to { runId, outcome: "routed", value: "billing" }.

A FlowFixture contains { version: 1, flow, steps }. It contains responses, not the original input or flow implementation. Replay checks the flow name, step IDs, and requested question names; it does not validate prompts or state. Missing responses reject rather than calling live inference. Map/loop steps use composed IDs such as rounds/0/batch/1/assess.

Tasks and request factories execute again. A task can still perform external calls or side effects, so use controlled tasks when replaying in tests. Failed or cancelled captures are partial fixtures, not replays of recorded failures. See the replay guide for fixture copying and the repository CLI.

Build a fixture (unreleased)#

flowFixture(definition, run, traces) is available from both jev-devtools and jev-devtools/flow. Pass the captured flow definition/run and a ReadonlyMap<string, Trace> indexed by trace ID. It returns a BuiltFlowFixture with { fixture, missing, warnings }.

Check missing for completed steps whose response was unavailable, and warnings for capture limitations before saving fixture as JSON. Server redaction and truncation remain in the fixture. The drawer's Copy fixture also applies its current display masking; calling this helper directly does not add display masking.

Return interface#

Property Type Description
fixture FlowFixture Captured response bodies indexed by step ID.
missing string[] Completed steps whose response captures were unavailable.
warnings string[] Capture limitations to inspect before saving.

FlowBuilder, FlowRunOptions, FlowOutcome, FlowTask, FlowRequestFactory, TaskContext, and Resolved describe builder and execution contracts. FlowDefinition, FlowRun, FlowStep, FlowDisplay, and FlowObserver describe inspection data and callbacks. FlowFixture and BuiltFlowFixture describe replay data (unreleased). See recorder and types.

Compare a fixture with a live run (unreleased)#

compareWithFixture runs the same recipe and input twice: first with saved answers, then with your supplied client. formatComparison turns the result into readable text. This comparison performs live inference; ordinary replayClient does not.

ts
import { compareWithFixture, formatComparison } from "jev-devtools/flow";

// program, input, fixture, client, and inspector come from your app.
const comparison = await compareWithFixture(program, input, fixture, {
  client,
  devtools: inspector,
  tolerance: 0.01,
});
console.log(formatComparison(comparison));

Example walkthrough: the comparison replays the stored answers, then runs with the supplied client using the same input. The tolerance applies to numeric answer-field differences. formatComparison() returns text, and the application's console.log() prints it.

Value / member Type Description
compareWithFixture(...) Promise<FixtureComparison> Resolves to outcome, value, decisions, answers, step differences, and a live fixture.
comparison.changed boolean Outcome, value, decision, or executed Jev-step differences.
comparison.answers AnswerChange[] Individual scalar answer-field comparisons.
comparison.liveFixture FlowFixture Unredacted, untruncated live response bodies.
formatComparison(comparison) string Plain-text description of changes.

Tasks execute on both sides. Use controlled, repeatable tasks; neither replay nor comparison undoes side effects. Top-level changed tracks outcome, value, decisions, and executed Jev steps; answer drift alone does not set it. Inspect answers for individual field differences. Numeric tolerance only applies to those answer fields. The returned liveFixture contains unredacted live responses, so review it before saving or sharing.

See FixtureComparison for every output field and the comparison contract for errors and cancellation.

Jev DevTools is an independent, open-source community project. It is not affiliated with TypeSafe AI.