flow() Workflows
Use flow() to compose typed programs into a multi-step workflow the application owns: explicit nodes, explicit state, deterministic branching, loops, and parallel execution.
String mermaid = "flowchart TD\n" +
" %%ax research: topicText:string -> factList:string[]\n" +
" %%ax audience: topicText:string -> audienceAngle:string\n" +
" %%ax join: factList:string[], audienceAngle:string -> briefText:string\n" +
" research & audience --> join";
AxFlow program = Ax.flow(mermaid);
System.out.println(program);Each node is a typed program built from a signature. The flow graph — not the model — decides what runs next, and independent steps run in parallel automatically when their state reads and writes do not conflict.
What It Does
flow() builds a workflow from typed nodes and mappings. Node results land in application-owned state, an execution planner groups independent steps for parallel execution, and the final mapping selects the public result.
flowchart LR A["Typed nodes"] --> B["Flow graph"] C["Application state"] --> B B --> D["Execution plan"] D --> E["Parallel groups"] E --> F["Node programs"] F --> C C --> G["Returns mapping"] G --> H["Typed result"]
Core Call Shape
wf = flow(options)
.node(name, signature)
.execute(name, stateToInputs)
.returns(stateToOutput)
result = wf.forward(aiClient, inputs)Common Patterns
- Give every node a typed contract; auto-wiring and parallelism both come from field names and declared reads/writes.
- Use
classdecision outputs to branch; downstream nodes consume the decision as a plainstring. - Keep loops capped: feedback edges and while loops both take an explicit max.
- Expose a finished workflow as a tool with
toFunction(), and tune the whole flow with the shared optimizer surface.
Rich node contracts
Node signatures accept the full string grammar — constraint bags, class decisions, optional fields, and nested objects:
triage: ticketText:string -> ticketClass:class "bug, billing, question", severityScore:number(min 1, max 5)
draft: ticketText:string, ticketClass:string, severityScore:number -> replyText:string(max 400)
audit: replyText:string -> approved:boolean, flaggedSpans:object{ spanText:string, reasonNote:string }[]Branch on a typed decision
Classify once, then feed the typed route into the responder. The checked-in example is provider-backed and runnable.
AxGen classifier = Ax.ax("request:string -> route:class \"support, sales, engineering\"");
AxGen responder = Ax.ax("request:string, route:string -> response:string");
AxFlow program = Ax.flow(Map.of("id", "examples.branchFlow"))
.execute("classifier", classifier, Map.of("reads", List.of("request"), "writes", List.of("classifierResult", "route")))
.execute("responder", responder, Map.of("reads", List.of("request", "route"), "writes", List.of("responderResult", "response")))
.returns(Map.of("route", "route", "response", "response"));Parallel fan-out and join
Independent research and audience nodes share the same input, so the planner runs them together before the join consumes both results.
AxFlow program = Ax.flow(Map.of("id", "examples.parallelFlow"))
.execute("research", research, Map.of("reads", List.of("topicText"), "writes", List.of("researchResult", "factList")))
.execute("audience", audience, Map.of("reads", List.of("topicText"), "writes", List.of("audienceResult", "audienceAngle")))
.execute("join", join, Map.of("reads", List.of("factList", "audienceAngle"), "writes", List.of("joinResult", "briefText")))
.returns(Map.of("briefText", "briefText"));Draft, critique, revise
Each stage consumes named output from the previous stage, making the refinement order explicit and observable.
AxFlow program = Ax.flow(Map.of("id", "examples.refineFlow"))
.execute("draft", draft, Map.of("reads", List.of("topicText"), "writes", List.of("draftResult", "draftText")))
.execute("critique", critique, Map.of("reads", List.of("draftText"), "writes", List.of("critiqueResult", "critiqueText")))
.execute("revise", revise, Map.of("reads", List.of("draftText", "critiqueText"), "writes", List.of("reviseResult", "revisedText")))
.returns(Map.of("revisedText", "revisedText"));Mermaid Source
A whole flow can be written as — or exported to — a mermaid flowchart. Node contracts travel in %%ax comment directives (any mermaid renderer ignores them), data auto-wires by field name to the nearest upstream producer, labeled edges out of a decision diamond become branches, and a back-edge is a capped loop.
Decision branch — a class diamond routes to per-branch responders, then re-joins:
flowchart TD
%%ax classify: requestText:string -> routeClass:class "support, sales"
%%ax supportReply: requestText:string -> replyText:string(max 300)
%%ax salesReply: requestText:string -> replyText:string(max 300)
%%ax send: replyText:string -> deliveredReply:string
classify{routeClass}
classify -->|support| supportReply
classify -->|sales| salesReply
supportReply --> send
salesReply --> sendFan-out / fan-in — two perspectives run in parallel, then a judge joins them:
flowchart TD
%%ax outline: topicText:string -> questionText:string
%%ax proponent: questionText:string -> proArgument:string
%%ax skeptic: questionText:string -> conArgument:string
%%ax judge: proArgument:string, conArgument:string -> verdictSummary:string
outline --> proponent & skeptic
proponent & skeptic --> judgeTicket triage, end to end — classify, draft, review with a capped revise loop, then send:
flowchart TD
%%ax triage: ticketText:string -> ticketClass:class "bug, billing, question", severityScore:number(min 1, max 5)
%%ax draft: ticketText:string, ticketClass:string, severityScore:number -> replyText:string(max 400)
%%ax review: replyText:string -> verdict:class "approve, revise", reviewNote?:string
%%ax finalize: replyText:string, reviewNote?:string -> finalMessage:string
triage[Triage ticket] --> draft --> review{verdict}
review -->|approve| finalize
review -->|revise, max 2| draftThree-way branch and re-join — triage routes to one of three handlers before delivery:
flowchart TD
%%ax triage: ticketText:string -> ticketClass:class "bug, billing, question"
%%ax bugHandler: ticketText:string -> replyText:string(max 300)
%%ax billingHandler: ticketText:string -> replyText:string(max 300)
%%ax questionHandler: ticketText:string -> replyText:string(max 300)
%%ax send: replyText:string -> deliveredReply:string
triage{ticketClass}
triage -->|bug| bugHandler
triage -->|billing| billingHandler
triage -->|question| questionHandler
bugHandler --> send
billingHandler --> send
questionHandler --> sendJudge panel — three independent drafts fan out, then converge on one verdict:
flowchart TD
%%ax outline: topicText:string -> outlineText:string
%%ax draftA: outlineText:string -> draftAText:string
%%ax draftB: outlineText:string -> draftBText:string
%%ax draftC: outlineText:string -> draftCText:string
%%ax judge: draftAText:string, draftBText:string, draftCText:string -> verdictText:string
outline --> draftA & draftB & draftC
draftA & draftB & draftC --> judgeEscalation ladder — a quality gate either sends the first answer or falls back to level two:
flowchart TD
%%ax l1Answer: ticketText:string -> answerText:string
%%ax qualityGate: answerText:string -> verdict:class "pass, escalate"
%%ax l2Answer: ticketText:string -> answerText:string
%%ax send: answerText:string -> deliveredAnswer:string
l1Answer --> qualityGate{verdict}
qualityGate -->|pass| send
qualityGate -->|escalate| l2Answer --> sendItinerary planner — rich contracts stay attached to a simple linear graph:
flowchart TD
%%ax parse: requestText:string -> destinationName:string, stayWindow:dateRange, travelerCount:number(min 1, max 12), budgetUsd?:number(min 0)
%%ax plan: destinationName:string, stayWindow:dateRange, travelerCount:number, budgetUsd?:number -> itineraryItems:object{ dayNumber:number(min 1), activityText:string }[]
%%ax price: itineraryItems:object{ dayNumber:number, activityText:string }[], travelerCount:number -> estimatedTotalUsd:number(min 0), bookingNotes?:string(max 300)
parse --> plan --> priceFan-out with capped revision — two sections join, then review can send the assembly back twice:
flowchart TD
%%ax outline: briefText:string -> outlineText:string
%%ax sectionA: outlineText:string -> sectionAText:string
%%ax sectionB: outlineText:string -> sectionBText:string
%%ax assemble: sectionAText:string, sectionBText:string -> articleText:string
%%ax review: articleText:string -> verdict:class "approve, revise", reviewNote?:string
%%ax publish: articleText:string, reviewNote?:string -> publishedArticle:string
outline --> sectionA & sectionB
sectionA & sectionB --> assemble --> review{verdict}
review -->|approve| publish
review -->|revise, max 2| assembleIn TypeScript, passing the diagram to flow() compiles it into a runnable flow, and String(wf) renders any flow back to the same dialect — so flow(String(wf)) round-trips (wf.toString({ direction: 'LR' }) for render options). Because the signature grammar is text-complete, the diagram is the entire program — writable by hand, by an LLM, or exported from a design doc.
Production Notes
Keep node contracts small and name fields by domain — auto-wiring and the execution plan both key off field names. Cap every loop. Trace flows like any program: node order, mappings, model calls, and tool activity all land in the shared telemetry surface. For work the model should plan itself, use agent(); keep flow() for orchestration the application must own.
See flow() API, s() Signatures, and Optimization.