| name | ax-flow |
| description | This skill helps an LLM generate correct AxFlow workflow code using @ax-llm/ax. Use when the user asks about flow(), AxFlow, workflow orchestration, parallel execution, DAG workflows, conditional routing, map/reduce patterns, or multi-node AI pipelines. |
| version | 19.0.17 |
AxFlow Codegen Rules (@ax-llm/ax)
Use this skill to generate AxFlow workflow code. Prefer short, modern, copyable patterns. Do not write tutorial prose unless the user explicitly asks for explanation.
Use These Defaults
- Use
flow() factory, not new AxFlow().
- Import:
import { ai, flow, f } from '@ax-llm/ax';
autoParallel: true is the default; independent executes run in parallel automatically.
- Node results are stored as
${nodeName}Result in state.
- Always define
.node() before .execute() for that node.
- Use
.returns() (or .r()) as the last step to lock the output type.
- Use descriptive node names:
documentSummarizer, not proc1.
- Use descriptive field names:
userInput, responseText, not text, result.
Critical Rules
- Use
flow() factory syntax for new code.
- Node results in state follow the pattern
state.${nodeName}Result.${fieldName}.
.execute() maps current state to node inputs; .map() transforms state without AI calls.
.returns() maps final state to the flow output type.
- Always define nodes before executing them; reversed order throws at runtime.
- Keep state flat; avoid deep nesting in
.map().
- Ensure loop conditions can change to avoid infinite loops.
- Structure independent executes to maximize auto-parallelization.
- Use
flow<InputType, OutputType>() for typed flows.
- Aliases:
.n() = .node(), .nx() = .nodeExtended(), .m() = .map(), .r() = .returns().
Canonical Pattern
import { ai, flow } from '@ax-llm/ax';
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
const wf = flow<{ userInput: string }, { responseText: string }>()
.node('testNode', 'userInput:string -> responseText:string')
.execute('testNode', (state) => ({ userInput: state.userInput }))
.returns((state) => ({ responseText: state.testNodeResult.responseText }));
const result = await wf.forward(llm, { userInput: 'Hello world' });
console.log(result.responseText);
Factory Options
const wf = flow();
const wf = flow({ autoParallel: false });
const wf = flow<InputType, OutputType>();
const wf = flow<InputType, OutputType>({ autoParallel: true, batchSize: 5 });
State Evolution
State grows with each executed node. Results are stored as ${nodeName}Result:
flow.execute('processor', (state) => ({ input: state.userInput }));
flow.execute('analyzer', (state) => ({ text: state.processorResult.output }));
Node Definition
flow.node('processor', 'input:string -> output:string');
flow.node('analyzer', 'text:string -> sentiment:string, confidence:number');
flow.node('extractor', 'documentText:string -> entities:string[]');
flow.n('processor', 'input:string -> output:string');
Extended Nodes (nx)
Add fields to a base signature without rewriting it:
import { f, flow } from '@ax-llm/ax';
flow.nx('reasoner', 'question:string -> answer:string', {
prependOutputs: [
{ name: 'reasoning', type: f.internal(f.string('Step-by-step reasoning')) },
],
});
flow.nx('analyzer', 'input:string -> result:string', {
appendOutputs: [{ name: 'confidence', type: f.number('Confidence 0-1') }],
});
flow.nx('processor', 'query:string -> response:string', {
appendInputs: [{ name: 'context', type: f.optional(f.string('Extra context')) }],
});
Extension options: prependInputs, appendInputs, prependOutputs, appendOutputs.
Execute With Input Mapping
flow.execute('summarizer', (state) => ({ documentText: state.document }));
flow.execute('processor', (state) => ({ input: state.data }), { ai: alternativeAI });
Map (State Transformation)
Use map() for data shaping without AI calls:
flow.map((state) => ({ ...state, upperText: state.rawText.toUpperCase() }));
flow.map(async (state) => {
const data = await fetchFromAPI(state.query);
return { ...state, enrichedData: data };
});
flow.map([
async (state) => ({ ...state, result1: await api1(state.data) }),
async (state) => ({ ...state, result2: await api2(state.data) }),
], { parallel: true });
Returns (Final Output)
const wf = flow<{ input: string }>()
.map((state) => ({ ...state, upper: state.input.toUpperCase(), len: state.input.length }))
.returns((state) => ({ upper: state.upper, isLong: state.len > 20 }));
const result = await wf.forward(llm, { input: 'test' });
Sequential Processing
const wf = flow<{ input: string }, { finalResult: string }>()
.node('step1', 'input:string -> intermediate:string')
.node('step2', 'intermediate:string -> output:string')
.execute('step1', (state) => ({ input: state.input }))
.execute('step2', (state) => ({ intermediate: state.step1Result.intermediate }))
.returns((state) => ({ finalResult: state.step2Result.output }));
Auto-Parallel Execution
Independent executes run in parallel automatically (autoParallel: true by default):
const wf = flow<{ text: string }, { combined: string }>()
.node('sentimentAnalyzer', 'text:string -> sentiment:string')
.node('topicExtractor', 'text:string -> topics:string[]')
.node('entityRecognizer', 'text:string -> entities:string[]')
.execute('sentimentAnalyzer', (state) => ({ text: state.text }))
.execute('topicExtractor', (state) => ({ text: state.text }))
.execute('entityRecognizer', (state) => ({ text: state.text }))
.returns((state) => ({
combined: JSON.stringify({
sentiment: state.sentimentAnalyzerResult.sentiment,
topics: state.topicExtractorResult.topics,
entities: state.entityRecognizerResult.entities,
}),
}));
const plan = wf.getExecutionPlan();
console.log(plan.parallelGroups, plan.maxParallelism);
Disable auto-parallel:
const wf = flow({ autoParallel: false });
await wf.forward(llm, input, { autoParallel: false });
Conditional Branching
const wf = flow<{ query: string; expertMode: boolean }, { response: string }>()
.node('simple', 'query:string -> response:string')
.node('expert', 'query:string -> response:string')
.branch((state) => state.expertMode)
.when(true)
.execute('expert', (state) => ({ query: state.query }))
.when(false)
.execute('simple', (state) => ({ query: state.query }))
.merge()
.returns((state) => ({
response: state.expertResult?.response ?? state.simpleResult?.response,
}));
After .merge(), only the taken branch's result exists; use optional chaining (?.) on untaken branch results.
While Loops
const wf = flow<{ content: string }, { finalContent: string }>()
.node('processor', 'content:string -> processedContent:string')
.node('qualityChecker', 'content:string -> qualityScore:number')
.map((state) => ({ currentContent: state.content, iteration: 0, qualityScore: 0 }))
.while((state) => state.iteration < 3 && state.qualityScore < 0.8)
.map((state) => ({ ...state, iteration: state.iteration + 1 }))
.execute('processor', (state) => ({ content: state.currentContent }))
.execute('qualityChecker', (state) => ({
content: state.processorResult.processedContent,
}))
.map((state) => ({
...state,
currentContent: state.processorResult.processedContent,
qualityScore: state.qualityCheckerResult.qualityScore,
}))
.endWhile()
.returns((state) => ({ finalContent: state.currentContent }));
Rules:
- Every
.while() needs a matching .endWhile().
- Ensure the loop condition can change to avoid infinite loops.
Feedback Loops (label/feedback)
const wf = flow<{ prompt: string }, { result: string }>()
.node('gen', 'prompt:string -> result:string, quality:number')
.map((state) => ({ ...state, tries: 0 }))
.label('retry')
.map((state) => ({ ...state, tries: state.tries + 1 }))
.execute('gen', (state) => ({ prompt: state.prompt }))
.feedback((state) => state.genResult.quality < 0.9 && state.tries < 3, 'retry')
.returns((state) => ({ result: state.genResult.result }));
Rules:
- Define the label before referencing it in
.feedback().
- Always include a max-iteration guard to avoid infinite loops.
Explicit Parallel Sub-Flows
flow
.parallel([
(sub) => sub.execute('analyzer1', (state) => ({ text: state.input })),
(sub) => sub.execute('analyzer2', (state) => ({ text: state.input })),
(sub) => sub.execute('analyzer3', (state) => ({ text: state.input })),
])
.merge('combinedResults', (r1, r2, r3) => ({
a1: r1.analyzer1Result.analysis,
a2: r2.analyzer2Result.analysis,
a3: r3.analyzer3Result.analysis,
}));
Derive (Batch/Array Processing)
const wf = flow<{ items: string[] }, { processed: string[] }>({ batchSize: 3 })
.derive('processed', 'items', (item, index) => `processed-${item}-${index}`, {
batchSize: 2,
});
Dynamic AI Context (Multi-Model)
Route nodes to different AI providers:
const fast = ai({ name: 'groq', apiKey: '...' });
const smart = ai({ name: 'anthropic', apiKey: '...' });
const wf = flow<{ text: string }, { out: string }>()
.node('draft', 'text:string -> out:string')
.node('refine', 'text:string -> out:string')
.execute('draft', (state) => ({ text: state.text }), { ai: fast })
.execute('refine', (state) => ({ text: state.draftResult.out }), { ai: smart })
.returns((state) => ({ out: state.refineResult.out }));
Description and toFunction
const wf = flow<{ userQuestion: string }, { responseText: string }>()
.node('qa', 'userQuestion:string -> responseText:string')
.execute('qa', (state) => ({ userQuestion: state.userQuestion }))
.returns((state) => ({ responseText: state.qaResult.responseText }))
.description('Question Answerer', 'Answers user questions concisely.');
const fn = wf.toFunction();
Instrumentation (Tracing)
import { ai, flow } from '@ax-llm/ax';
import { context, trace } from '@opentelemetry/api';
const tracer = trace.getTracer('axflow');
const llm = ai({ name: 'openai', apiKey: '...' });
const wf = flow<{ userQuestion: string }>()
.node('summarizer', 'documentText:string -> summaryText:string')
.execute('summarizer', (s) => ({ documentText: s.userQuestion }))
.returns((s) => ({ answer: s.summarizerResult.summaryText }));
const result = await wf.forward(llm, { userQuestion: 'hi' }, {
tracer,
traceContext: context.active(),
});
Program IDs and Demos
const wf = flow<{ input: string }>()
.node('summarizer', 'text:string -> summary:string')
.node('classifier', 'text:string -> category:string');
console.log(wf.namedPrograms());
wf.setDemos([{ programId: 'root.summarizer', traces: [] }]);
wf.applyOptimization(optimizedProgram);
Error Handling
try {
const result = await wf.forward(llm, input);
} catch (error) {
console.error('Flow execution failed:', error);
}
Common errors:
"Node 'x' not found" -- define .node() before .execute().
"endWhile() without matching while()" -- every .while() needs .endWhile().
"when() without matching branch()" -- .when() must be inside .branch()/.merge().
"merge() without matching branch()" -- every .branch() needs .merge().
"Label 'x' not found" -- define .label() before .feedback() references it.
Examples
Fetch these for full working code:
Do Not Generate
- Do not use
new AxFlow(...) for new code.
- Do not execute a node before defining it with
.node().
- Do not use generic field names like
text, result, data, input, output.
- Do not create deep-nested state objects in
.map().
- Do not create loop conditions that can never change.
- Do not add unnecessary dependencies between executes (kills auto-parallelism).
- Do not forget to use optional chaining on branch results after
.merge().