TypeScript API: Patterns
Ready-made multi-agent designs: sequential, parallel, loops, routers, debates and more. Generated from the declarations and doc comments of criogenai 0.1.7. See also: TypeScript API overview, the same module in Python.
Design patterns for agentic systems, ready to use.
import { patterns } from "criogenai";
const flow = patterns.quick("researcher > writer", { model: "openai:gpt-5-mini" });
console.log(String(await flow.run("Write about solar panels")));Steps can be an Agent, a Specialist or its registered name (a string), a
plain function text => text (sync or async), or [name, step]. The model
for specialists comes from model or the CRIOGEN_MODEL environment
variable. Every loop has a cap, so nothing runs forever.
Workflow patterns return a Pattern: run for the answer, compile(storage)
for durable threads, approvals and an audit trail, workflow to extend the
graph. react, coordinator and hierarchical return an Agent, so they
nest as steps of other patterns.
import { patterns } from "criogenai";
patterns.sequentialThese are used as patterns.<name>.
Contents#
| Name | Kind | Summary |
|---|---|---|
sequential | function | Runs the steps in a fixed order; each one's output is the next one's input. |
parallel | function | Runs the branches at the same time on the same input, then combines them: with a synthesizer step (it receives the input and every branch's answer), or as a titled list in branch order. |
loop | function | Repeats the steps (a chain) until until(text, round) is true or maxIterations is reached (default 5, at most 50). |
custom | function | Custom logic: any Workflow, with the same run / compile as the ready-made patterns. |
CEILING | constant | No loop runs more often than this, whatever the caller asks for. |
DEFAULT_MODEL_ENV | constant | The environment variable that names the model for specialists when a pattern gets no model. |
Step | type | One step of a pattern: an Agent, a Specialist, a registered specialist's name (a string), a function text => text (sync or async), or [name, step] to name the step in the trace. |
PatternResult | class | What a pattern produced. |
Pattern | class | A workflow pattern: run it, compile it for durability, or extend it. |
iterativeRefinement | function | Improves a result step by step: refine gets the current text and returns a better one; stops when score(text) >= threshold or after maxIterations. |
reviewAndCritique | function | A generator writes, a critic reviews. |
swarm | function | Agents hand the work to each other with no central coordinator. |
humanReview | function | Runs a step, then pauses for a person to look at its draft. |
quick | function | A pattern from one line of registered specialist names (plug and play): |
react | function | A single agent in a reason-act loop with a hard cap on model calls (default 8, at most 50). |
coordinator | function | An agent that reads each request and hands it to the right specialist, which it calls as a tool. |
hierarchical | function | A coordinator of coordinators: the root splits the work, each level hands it further down, workers do it. |
sequential#
function sequential(steps: Step[], options?: { model?: Model; }): PatternRuns the steps in a fixed order; each one's output is the next one's input. quick("researcher > writer") is the one-liner.
Parameters
steps(Step[]): At least one step.options({ model?: Model; }, default{}):model: the model for specialist steps (else theCRIOGEN_MODELenvironment variable).
Raises
Error: No steps, or a specialist step and no model.TypeError: A step is not an Agent, a Specialist (or its name) or a function.
Example
import { patterns } from "criogenai";
const flow = patterns.sequential([
["trim", (text: string) => text.trim()],
["shout", async (text: string) => text.toUpperCase()],
]);
const result = await flow.run(" hello ");
console.log(String(result)); // "HELLO"
console.log(result.explain()); // ended: done, then one line per stepparallel#
function parallel(branches: Step[] | Record<string, Step>, options?: { synthesizer?: Step; model?: Model; }): PatternRuns the branches at the same time on the same input, then combines them:
with a synthesizer step (it receives the input and every branch's answer),
or as a titled list in branch order.
Parameters
branches(Step[] | Record<string, Step>): The steps, as a list or as{ name: step }(the names title the combined list).options({ synthesizer?: Step; model?: Model; }, default{}):synthesizer: a step that writes one answer from the request and the branches' answers.model: the model for specialist steps.
Raises
Error: No branches.
Example
import { patterns } from "criogenai";
const flow = patterns.parallel({
words: (text: string) => `${text.split(" ").length} words`,
letters: (text: string) => `${text.length} characters`,
});
console.log(String(await flow.run("the quick brown fox")));
// ## words
// 4 words
//
// ## letters
// 19 charactersloop#
function loop(steps: Step[], until: (text: string, round: number) => boolean, options?: { maxIterations?: number; model?: Model; }): PatternRepeats the steps (a chain) until until(text, round) is true or
maxIterations is reached (default 5, at most 50). stopReason says which.
Parameters
steps(Step[]): The chain to repeat; each round starts from the last round's output.until((text: string, round: number) => boolean): Checked after each round with the output and the round number (from 1):truestops (stopReason"condition").options({ maxIterations?: number; model?: Model; }, default{}):maxIterations(a whole number of at least 1; values above 50 count as 50) andmodelfor specialist steps.
Raises
RangeError:maxIterationsis not a whole number of at least 1.
Example
import { patterns } from "criogenai";
const flow = patterns.loop([(text: string) => text + "!"], (text) => text.endsWith("!!!"), { maxIterations: 10 });
const result = await flow.run("go");
console.log(String(result), result.stopReason); // "go!!!" "condition"custom#
function custom(workflow: Workflow, options?: { name?: string; maxSteps?: number; }): PatternCustom logic: any Workflow, with the same run / compile as the ready-made patterns. Start state { input }; the answer is state.output.
Parameters
workflow(Workflow): A workflow whose nodes readstate.inputand writestate.output(and optionallytraceandstop_reason).options({ name?: string; maxSteps?: number; }, default{}):name(default"custom") andmaxSteps, the step limit forrun(default 64).
Example
import { END, Workflow, patterns } from "criogenai";
const wf = new Workflow()
.node("shout", (state) => ({ output: String(state.input).toUpperCase() }))
.start("shout")
.edge("shout", END);
const result = await patterns.custom(wf).run("hello");
console.log(String(result)); // "HELLO"CEILING#
const CEILING: 50No loop runs more often than this, whatever the caller asks for.
DEFAULT_MODEL_ENV#
const DEFAULT_MODEL_ENV: "CRIOGEN_MODEL"The environment variable that names the model for specialists when a pattern gets no model.
Step#
type Step = Agent<any> | specialists.Specialist | string | ((text: string) => unknown) | [string, Step];One step of a pattern: an Agent, a Specialist, a registered specialist's
name (a string), a function text => text (sync or async), or
[name, step] to name the step in the trace.
PatternResult#
class PatternResult {
constructor(output: string | null, trace: Entry[], state: State, stopReason: string, suspended: boolean, waitingFor: unknown, raw: WorkflowResult);
}What a pattern produced. String(result) is the output; result.explain() shows each step.
Parameters
output(string | null): The final answer, ornullwhile the run waits for a person. The final answer, ornullwhile suspended.trace(Entry[]): Each step and what it returned ({ step, output }), in order. Each step and what it returned, in order.state(State): The workflow's final state.stopReason(string): Why it ended: done, condition, approved, max_iterations, max_rounds, final, max_handoffs, unknown_handoff, rejected or suspended. Why it ended.suspended(boolean):truewhen ahumanReviewrun waits for a person: continue withpattern.resume.truewhen a person must answer first.waitingFor(unknown): What a person is asked, when suspended. What the person is asked, when suspended.raw(WorkflowResult): The workflow result it was made from.
PatternResult.toString#
toString(): stringThe output as text ("" when there is none), so String(result) is the answer.
PatternResult.explain#
explain(): stringEach step on its own line: which step, what it returned (shortened).
Pattern#
class Pattern {
constructor(name: string, workflow: Workflow, budget: number);
}A workflow pattern: run it, compile it for durability, or extend it.
Returned by sequential, parallel, loop, iterativeRefinement,
reviewAndCritique, swarm, humanReview, quick and custom.
Parameters
name(string): The pattern's name, e.g."sequential"or"review_and_critique". The pattern's name, e.g."sequential".workflow(Workflow): The plain Workflow: add nodes and edges to extend it before the first run. The workflow that implements it.budget(number): The step limit forrunwhen none is given.
Pattern.compile#
compile(storage?: string | Storage): CompiledWorkflowThe checked, durable form: compile(storage).thread(id).run({ input }), with approvals, resume and an audit trail.
The state starts as { input }; the answer is in state.output.
Parameters
storage(string | Storage, optional): AStorageor a URL for the threads; in memory without one.
Raises
ValidationError: The workflow (after your changes) does not validate.
Pattern.run#
run(text: string, options?: { maxSteps?: number; signal?: any; }): Promise<PatternResult>Runs once. A humanReview pattern returns suspended: true; continue with resume.
Parameters
text(string): The request: the first step's input.options({ maxSteps?: number; signal?: any; }, default{}):maxSteps: the run's step limit (each pattern sets one from its own caps).signal: aborting it cancels the run.
Returns: The answer (String(result)), the trace and why it ended.
Pattern.resume#
resume(paused: PatternResult, decision: unknown): Promise<PatternResult>Continues a humanReview run: "approve", { decision: "edit", text } or { decision: "reject" }.
Works on the result of this object's own run. For a pause that must
outlive the process, use compile(storage).thread(id) instead.
Parameters
paused(PatternResult): The suspended result ofrun.decision(unknown):"approve"keeps the draft,{ decision: "edit", text }replaces it,{ decision: "reject" }ends with an empty output andstopReason"rejected".
Raises
Error:pausedis not a run of this pattern that waits for a person.
iterativeRefinement#
function iterativeRefinement(refine: Step, score: (text: string) => number, options?: { threshold?: number; maxIterations?: number; model?: Model; }): PatternImproves a result step by step: refine gets the current text and returns a better one; stops when score(text) >= threshold or after maxIterations.
Parameters
refine(Step): The step that rewrites the text.score((text: string) => number): Rates a text, typically from 0 to 1.options({ threshold?: number; maxIterations?: number; model?: Model; }, default{}):threshold(default 0.8),maxIterations(default 5, at most 50) andmodelfor a specialist step.
Example
import { patterns } from "criogenai";
const flow = patterns.iterativeRefinement(
(text: string) => text + " More detail.",
(text) => Math.min(text.length / 40, 1),
);
console.log((await flow.run("A short note.")).stopReason); // "condition"reviewAndCritique#
function reviewAndCritique(generator: Step, critic: Step, options?: { maxRounds?: number; approved?: ((reply: string) => boolean); model?: Model; }): PatternA generator writes, a critic reviews. The critic approves (its reply starts
with APPROVED, or approved(reply) is true) or returns feedback; the
generator revises. Ends on approval or after maxRounds (default 3, at most
50) with the last draft. stopReason is "approved" or "max_rounds".
Parameters
generator(Step): The step that writes and revises. From the second round it gets the request, its previous draft and the critic's feedback.critic(Step): The step that reviews; it gets the request and the draft.options({ maxRounds?: number; approved?: ((reply: string) => boolean); model?: Model; }, default{}):maxRounds(default 3, at most 50),approved(decides from the critic's reply; default: it starts with "APPROVED"),modelfor specialist steps.
Raises
RangeError:maxRoundsis not a whole number of at least 1.
Example
import { Agent, FakeModel, patterns } from "criogenai";
const writer = new Agent({ model: new FakeModel(["Draft one.", "Draft two, with sources."]) });
const critic = new Agent({ model: new FakeModel(["Add sources.", "APPROVED"]) });
const result = await patterns.reviewAndCritique(writer, critic).run("Write a short note on hand washing.");
console.log(String(result), result.stopReason); // "Draft two, with sources." "approved"swarm#
function swarm(agents: Record<string, Step>, options?: { start?: Step; maxHandoffs?: number; model?: Model; }): PatternAgents hand the work to each other with no central coordinator. Each agent is
told who it can hand off to and ends its reply with HANDOFF: <name> to pass
the work on or FINAL when the answer is ready. start names the first agent
(or is a dispatcher step that replies with a name); default: the first. Ends
on FINAL or after maxHandoffs (default 6, at most 50); a hand-off to an
unknown name ends the run with stopReason "unknown_handoff".
Parameters
agents(Record<string, Step>): At least two named steps,{ name: step }.options({ start?: Step; maxHandoffs?: number; model?: Model; }, default{}):start: the first agent's name, or a dispatcher step that replies with a name.maxHandoffs(default 6, at most 50).modelfor specialist steps.
Raises
Error: Fewer than two agents.RangeError:maxHandoffsis not a whole number of at least 1.
Example
import { Agent, FakeModel, patterns } from "criogenai";
const triage = new Agent({ model: new FakeModel(["This is a billing question.\nHANDOFF: billing"]) });
const billing = new Agent({ model: new FakeModel(["Your invoice was sent on Monday.\nFINAL"]) });
const result = await patterns.swarm({ triage, billing }).run("Where is my invoice?");
console.log(result.stopReason, result.trace.map((t) => t.step)); // "final" ["triage", "billing"]humanReview#
function humanReview(step: Step, options?: { model?: Model; }): PatternRuns a step, then pauses for a person to look at its draft. Resume with
"approve", { decision: "edit", text } or { decision: "reject" }. With
compile(storage) the pause is durable: another process can resume it days later.
While paused, waitingFor is { draft, question }.
Parameters
step(Step): The step that writes the draft.options({ model?: Model; }, default{}):modelfor a specialist step.
Example
import { patterns } from "criogenai";
const flow = patterns.humanReview((text: string) => `Reply: ${text}`);
const paused = await flow.run("Thanks for your order.");
console.log(paused.suspended, paused.waitingFor); // true { draft: "Reply: ...", question: "..." }
const done = await flow.resume(paused, { decision: "edit", text: "Thank you for your order!" });
console.log(String(done), done.stopReason); // "Thank you for your order!" "approved"quick#
function quick(spec: string, options?: { model?: Model; }): PatternA pattern from one line of registered specialist names (plug and play):
quick("researcher > writer") one after the other
quick("sentiment | keywords") at the same time
quick("writer <-> critic") write, review, revise until approved
quick("summarizer") a single specialistOne kind of connector per line (combine patterns with the functions above).
The specialists need a model: options.model, or the CRIOGEN_MODEL
environment variable.
Parameters
spec(string): The line, e.g."researcher > writer".options({ model?: Model; }, default{}):model: a model or a model string such as"openai:gpt-5-mini".
Raises
Error: The line is empty, mixes connectors, has an empty name, or uses<->with other than two names; a name is not registered; or there is no model.
Example
import { patterns } from "criogenai";
// Needs OPENAI_API_KEY.
const flow = patterns.quick("researcher > writer", { model: "openai:gpt-5-mini" });
const result = await flow.run("Write about solar panels");
console.log(String(result));
console.log(result.explain());react#
function react(model: Model, tools: (AgentTool | Tool<any>)[], options?: AgentExtras & { maxSteps?: number; instructions?: string; }): Agent<any>A single agent in a reason-act loop with a hard cap on model calls (default 8, at most 50).
Returns an Agent, so it can be a step of another pattern.
Parameters
model(Model): The agent's model.tools((AgentTool | Tool<any>)[]): The tools it may call.options(AgentExtras & { maxSteps?: number; instructions?: string; }, default{}): Any otherAgentoption, plusmaxSteps(the cap on model calls) andinstructions(put before the reason-act instructions).
Raises
RangeError:maxStepsis not a whole number of at least 1.
Example
import { FakeModel, FakeReply, patterns, tool } from "criogenai";
const lookup = tool({
name: "order_status",
description: "Status of an order.",
input: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
run: async ({ id }: { id: string }) => ({ id, status: "shipped" }),
});
const model = new FakeModel([FakeReply.toolCall("order_status", { id: "A-1" }), "Order A-1 has shipped."]);
const agent = patterns.react(model, [lookup], { maxSteps: 4 });
console.log((await agent.run("Where is order A-1?")).text);coordinator#
function coordinator(model: Model, team: Record<string, string | Agent<any> | Specialist | [string | Agent<any> | Specialist, string]>, options?: AgentExtras & { instructions?: string; }): Agent<any>An agent that reads each request and hands it to the right specialist, which
it calls as a tool. specialists: { name: Agent | Specialist | "registered-name" | [Agent, "when to use it"] }.
A model decides the routing (more flexible than a fixed workflow, and more model calls).
Returns an Agent; each specialist becomes one of its tools.
Parameters
model(Model): The coordinator's model (also used to build specialists given as aSpecialistor a name).team(Record<string, string | Agent<any> | Specialist | [string | Agent<any> | Specialist, string]>): The specialists by tool name. A description (from the pair, or the Specialist's own) tells the model when to call each.options(AgentExtras & { instructions?: string; }, default{}): Any otherAgentoption, plusinstructionsto replace the default coordinator instructions.
Raises
Error: The team is empty, or a name is not a registered specialist.TypeError: A team member is not an Agent, a Specialist or its name.
Example
import { Agent, FakeModel, FakeReply, patterns } from "criogenai";
const billing = new Agent({ model: new FakeModel(["Your invoice was sent on Monday."]) });
const lead = patterns.coordinator(
new FakeModel([FakeReply.toolCall("billing", { input: "Where is my invoice?" }), "It was sent on Monday."]),
{ billing: [billing, "Invoices and payments"] },
);
console.log((await lead.run("Where is my invoice?")).text);hierarchical#
function hierarchical(model: Model, tree: Record<string, TreeNode>, options?: AgentExtras & { instructions?: string; }): Agent<any>A coordinator of coordinators: the root splits the work, each level hands it
further down, workers do it. tree maps names to a worker (as for
coordinator) or to a nested object, which becomes a sub-coordinator
(describe it with a _description key).
Returns the root coordinator, an Agent.
Parameters
model(Model): The model of every coordinator (and of specialists given as aSpecialistor a name).tree(Record<string, TreeNode>): Names mapped to workers or to nested objects (sub-teams).options(AgentExtras & { instructions?: string; }, default{}): Any otherAgentoption for the root, plus itsinstructions.
Example
import { patterns } from "criogenai";
// Needs OPENAI_API_KEY; "researcher" and "writer" are registered specialists.
const lead = patterns.hierarchical("openai:gpt-5-mini", {
research: { _description: "Finds facts", web: "researcher" },
writing: { _description: "Writes the text", writer: "writer" },
});
console.log((await lead.run("A one-page brief on heat pumps")).text);