AI Agent
Use the built-in AIAgent node for structured prompting, multi-turn tool use, node-backed tools, and explicit execution guardrails.
AIAgent is the built-in node for model-backed workflow steps.
Use it when a workflow item needs:
- classification
- extraction
- routing
- summarization
- tool-backed reasoning
The agent still runs once per workflow item, with explicit message authoring, multi-turn tool use, and optional turn limits.
Using workflow().agent(...)
workflow().agent(...) accepts messages and passes them to the agent node.
Message authoring follows the agent message model:
- use a static array for fixed messages
- use
itemExpr(...)for per-item message generation - use the object form when you need
promptplusbuildMessages
If you want to reshape workflow data before the agent runs, use fluent DSL helpers such as .map((item, ctx) => ...): read the current row from item.json, and use ctx.data when prompt preparation depends on earlier completed steps. Keep itemExpr(...) for agent config fields like messages.
workflow("wf.example")
.manualTrigger<{ subject: string; route: string }>("Start", [
{
subject: "hello",
route: "sales",
},
])
.agent("Summarize", {
messages: itemExpr(({ item }) => [
{
role: "system",
content: 'Return strict JSON only: {"summary": string}',
},
{
role: "user",
content: `Subject: ${item.json.subject}\nRoute: ${item.json.route}`,
},
]),
model: "openai:gpt-4o-mini",
outputSchema: z.object({
summary: z.string(),
}),
})
.build();workflow().agent(...) is the fluent workflow authoring form of the agent step. new AIAgent(...) exposes the same message model as a direct node config.
Configuration
AIAgent is constructed with an options object. You always supply name, messages, and chatModel; optional fields include tools, outputSchema, guardrails, id, and retryPolicy.
const classifyMailOutputSchema = z.object({
outcome: z.enum(["rfq", "other"]),
summary: z.string(),
});
new AIAgent<{ subject: string; body: string }, z.output<typeof classifyMailOutputSchema>>({
name: "Classify RFQ vs other",
messages: [
{
role: "system",
content: "You triage incoming mail.",
},
{
role: "user",
content: ({ item }) => JSON.stringify(item.json),
},
],
chatModel: new OpenAIChatModelConfig("OpenAI", "gpt-4o-mini"),
outputSchema: classifyMailOutputSchema,
guardrails: {
maxTurns: 10,
},
});The workflow helper forwards the same runtime contract:
workflow("wf.mail")
.name("Mail classifier")
.manualTrigger({ subject: "RFQ", body: "Need quote" })
.agent("Classify", {
prompt: (item) => JSON.stringify(item),
model: "openai:gpt-4o-mini",
outputSchema: z.object({
outcome: z.enum(["rfq", "other"]),
summary: z.string(),
}),
});Message authoring
messages is usually a plain array of { role, content } in chat order. Each content is either a string or a function (args) => string that receives the current item, index, batch, and execution context—handy for serializing item.json.
When you need both fixed lines and a separate buildMessages callback (appended after prompt), use an object:
prompt: optional ordered lines (same shape as the array form)buildMessages: optional callback returning extraAgentMessageDtorows
The supported roles are:
systemuserassistant
Example with prompt plus buildMessages:
new AIAgent({
name: "Prepare support response",
messages: {
prompt: [
{ role: "system", content: "Answer as a support triage assistant. Return JSON only." },
{ role: "user", content: ({ item }) => `Inbound mail:\n${item.json.body}` },
],
buildMessages: ({ itemIndex, items }) => [
{
role: "assistant",
content: `This is item ${itemIndex + 1} of ${items.length}.`,
},
],
},
chatModel: new OpenAIChatModelConfig("OpenAI", "gpt-4o-mini"),
});Use a plain array when everything fits one list. Use the object form when you want callback-built messages appended after prompt.
For workflow-defined agents, prefer itemExpr(...) when messages depend on the current item.
Recommended output pattern
For workflow automation, return compact JSON instead of prose whenever possible.
Good examples:
{ "outcome": "rfq", "reasoning": "..." }{ "route": "support", "priority": "high" }{ "customerName": "...", "invoiceNumber": "..." }
This keeps downstream If, MapData, HttpRequest, and custom nodes deterministic.
Structured output enforcement
Use outputSchema when the agent result must be a validated runtime contract rather than a best-effort prompt convention.
With outputSchema set:
- the final agent result must validate against the schema
- supported models use native structured output for the final answer when possible
- unsupported models fall back to parse, validate, and repair retries
- invalid final output raises an error instead of silently returning text
Without outputSchema, the legacy behavior remains unchanged.
Tools
An agent can attach normal tool configs or node-backed tools.
Two rules still matter:
- tool names must be unique inside the agent
- multiple tool calls in one round are executed in parallel
When to use a normal custom tool
Use a normal ToolConfig + Tool implementation when:
- the capability only exists for agent use
- the input and output shape is tool-specific
- the runtime does not naturally belong in a reusable workflow node
When to use a callable tool (inline DSL)
Use callableTool(...) from @codemation/core when:
- the logic is app-local and small (deterministic helpers, formatting, merging fields)
- you want one object with
inputSchema,outputSchema, andexecutewithout a separate@tool()class - you may still declare
credentialRequirementson the callable config for credential slots
Callable tools are not nested agents: they always attach as normal tools. They do not auto-merge the current workflow item into tool input (unlike the default AgentToolFactory.asTool(...) behavior for nested agent nodes). Use execute({ input, item, ctx, ... }) and merge explicitly when needed.
When to use a node-backed tool
Use a node-backed tool when you already have a runnable node and want to expose it to the agent without writing a second adapter class.
If you want the shortest recipe for that pattern, see Use a Node as an Agent Tool.
The default adapter behavior is:
- the tool input becomes one node input item
- the wrapped node runs through DI using its normal node token
- the first
mainoutput item becomes the tool result
Example:
const lookupCustomerTool = AgentToolFactory.asTool(new LookupCustomerNodeConfig("Lookup customer"), {
name: "lookup_customer",
description: "Look up the current customer record.",
inputSchema: z.object({
customerId: z.string(),
}),
outputSchema: z.object({
customerName: z.string(),
accountTier: z.string(),
}),
});
new AIAgent({
name: "Answer with customer context",
messages: [
{ role: "system", content: "Use tools when needed. Return JSON only." },
{ role: "user", content: ({ item }) => JSON.stringify(item.json) },
],
chatModel: new OpenAIChatModelConfig("OpenAI", "gpt-4o-mini"),
tools: [lookupCustomerTool],
});Reusing the current workflow item in tool input
Use mapInput when the model supplies only part of the node input and the rest should come from the current workflow item.
const classifyMailTool = AgentToolFactory.asTool(new ClassifyMailNodeConfig("Classify mail"), {
name: "classify_mail",
description: "Classify the current mail as RFQ or not.",
inputSchema: z.object({
bodyHint: z.string(),
}),
outputSchema: z.object({
isRfq: z.boolean(),
reason: z.string(),
}),
mapInput: ({ input, item }) => ({
subject: String(item.json.subject ?? ""),
body: input.bodyHint,
}),
});Use mapOutput when the wrapped node returns more than the tool should expose back to the model.
Credentials
Chat models and tools can both declare credential requirements.
That means one agent can have:
- one language model credential slot
- zero or more tool credential slots
Each attachment is tracked as its own connection-owned child node in the workflow graph and runtime state.
Guardrails
Agent-level guardrails are configured on guardrails.
The main control is maxTurns: each turn is one model invocation for the item (including a round that only plans tool calls). Tool calls in parallel still count as part of that turn; the next turn starts after tool results are appended to the conversation.
Default: maxTurns defaults to 10—a practical safety budget so the model can alternate between answering and calling tools without running forever (many tool-agent UIs use a similar default iteration cap). If the model still wants to call tools when the cap is hit, Codemation either throws (onTurnLimitReached: "error", the default) or returns the last assistant message ("respondWithLastMessage"), depending on configuration.
new AIAgent({
name: "Support agent",
messages: [
{ role: "system", content: "Use tools carefully. Return JSON only." },
{ role: "user", content: ({ item }) => JSON.stringify(item.json) },
],
chatModel: new OpenAIChatModelConfig("OpenAI", "gpt-4o-mini"),
guardrails: {
maxTurns: 3,
onTurnLimitReached: "error",
modelInvocationOptions: {
maxTokens: 800,
},
},
});Use guardrails to control:
- how many model rounds the agent may take per item
- what happens if the turn cap is reached while tool calls are still pending
- per-call model invocation options when the provider supports them
If you omit guardrails, the built-in default remains bounded (10 turns) rather than unbounded.
Practical patterns
Classification
new AIAgent({
name: "Classify inbox message",
messages: [
{
role: "system",
content: 'Return strict JSON only. Shape: {"category":"sales"|"support"|"spam","reasoning":"..."}',
},
{
role: "user",
content: ({ item }) =>
JSON.stringify({
subject: item.json.subject,
body: item.json.body,
}),
},
],
chatModel: new OpenAIChatModelConfig("OpenAI", "gpt-4o-mini"),
});Tool-backed answering
new AIAgent({
name: "Answer with policy context",
messages: [
{ role: "system", content: "Use tools for policy lookup. Return JSON only." },
{ role: "user", content: ({ item }) => JSON.stringify(item.json) },
],
chatModel: new OpenAIChatModelConfig("OpenAI", "gpt-4o-mini"),
tools: [lookupCustomerTool, searchDocsTool],
outputSchema: z.object({
answer: z.string(),
recommendedPlan: z.string(),
monthlyPrice: z.number(),
}),
guardrails: {
maxTurns: 3,
},
});What comes out of the node
The agent still emits workflow items on main.
In practice:
- without
outputSchema, plain text becomes{ output: "..." }and valid JSON is parsed when possible - with
outputSchema, the node emits validated structured JSON or throws - tool-enabled flows still end with one final agent response, but structured finalization now runs after the tool loop completes
Good defaults
- Keep prompts narrow and domain-specific.
- Prefer strict JSON outputs for automation.
- Use node-backed tools when you already have a good reusable node.
- Use custom tools when the capability is agent-only.
- Lower
maxTurnswhen the task should be a single-shot classification; raise it when the model needs several tool rounds (defaults are aligned with common “max iterations” tooling defaults). - Follow
AIAgentwith deterministic nodes that act on the result.