Skip to main content
A model-driven workflow calls a chat model from a node, lets the model ask for tools, and keeps the model on a short leash with budgets and filters. The plugins involved (LLM_CALL, MCP_LIST_TOOLS, MCP_TOOL_CALL, SESSION_MEMORY and the decision plugins) are optional modules chosen when the server is built. See Built-in plugins. Yukta keeps the control flow in the graph, not in the model. The model suggests an action; a node you wired runs it, and you decide what happens next. That makes an agent easy to inspect, limit, pause and approve like any other workflow. This page shows how to put the pieces together. For the options of each plugin, see AI plugins and Decision plugins.

The pieces

Calling a model

LLM_CALL supports three kinds of provider: Give a key as decrypted:<key> so the secret is read from the environment variable YUKTA_SECRET_<KEY> and not stored in the workflow. The prompt can be built from the message with ${payload.message} and the system prompt is a plain option. The answer is in payload.content, with token usage in payload.usage. A failed call fails the node by default. With failureMode CONTINUE and routeByStatus on, the node emits on success or failure, so the failure port can lead to a second LLM_CALL with a different model as a fallback. See Failure handling.

Chat with memory

A chat turn is a SESSION_MEMORY node in load mode, the model, and a SESSION_MEMORY node in append mode:
Start it with a payload such as {"sessionId": "user-42", "message": "My name is Ada."}. A later start with the same sessionId sees the earlier turns in payload['history']. messages is written as an expression on purpose: a ${...} reference would turn the list into text.

Tools and the agent loop

An agent loop has four steps: the model reads the task and the tools on offer, decides whether it needs a tool, the workflow runs the tool, and the result goes back to the model.
  • MCP_LIST_TOOLS reads the tool names, descriptions and input schemas of an MCP server. allowedTools narrows the list.
  • LLM_CALL takes them in its tools option (an expression such as payload.tools). If the model wants a tool, the call appears in payload.actions as {"tool", "arguments", "id"}. The node does not run it.
  • A BRANCH node tests whether payload.actions has entries.
  • MCP_TOOL_CALL runs the tool. Give it allowedTools so that a model can never reach a tool you did not intend, even by choosing a name.
  • The result, in payload.text and payload.structured, goes into a second LLM_CALL that writes the answer.
A graph has no cycles, so an agent that may need several rounds is built in one of two ways. Unroll a fixed number of rounds by repeating the LLM_CALL, BRANCH and MCP_TOOL_CALL group, which gives a hard upper bound that is easy to read. Or move one round into its own workflow and run it again with SUB_WORKFLOW; nesting is capped at 5 levels, which bounds the loop. See Sub-workflows. LOOP_PREDICATE repeats a single processor, so it suits polling and retrying one call, not a whole round. Each node replaces the payload with its own result, so carry what you still need in the message metadata. The example below sets the task into the metadata and reads it again with ${metadata.task}.

Complete example

This session runs a one-tool agent against a hypothetical issue tracker. The first LLM_CALL may ask for one tool. If it does, the workflow runs the tool and a second LLM_CALL writes the answer; if not, the first answer is final. Both model nodes and the tool node share one budget named triage, and hardStop ends the whole execution if it is used up.
Create the session with POST /api/sessions, then start the workflow with POST /api/workflow/start and the body {"sessionId": "issue-agent", "workflowId": "triage"}. The tool server, tool names and token here are placeholders for your own. The prices are per million tokens; use your provider’s current prices. Things to notice:
  • If the model asks for several tools, pick takes the first one. To run all of them, split payload.actions with a SPLITTER node first.
  • The agent name puts the three nodes in one budget. Without it, each execution has one budget for all its nodes.
  • A trigger-set metadata.task survives every node, so the final prompt can quote the task.

Decisions instead of free text

When a node must choose, not write, a decision plugin is cheaper and easier to route on than a free-text answer: CLASSIFY gives one label from your list, GATE a yes/no with a probability, SCORE a position on a scale, ASK several answers in one call. They use a small local decision model (Ollama System One) or any chat model. Typical uses:
  • CLASSIFY with routeByLabel at the start of a workflow, so each kind of request takes its own branch, with an uncertain port for the unclear ones.
  • A GATE in front of MCP_TOOL_CALL that asks whether the arguments the model proposed are safe, with state set to payload.args.
  • ANOMALY and DEDUPE in a long-running, scheduled workflow, to react to an unusual or repeated event. See Scheduling.

Guardrails

Models can loop, overspend and be fed hostile input. Yukta gives you limits at four levels. A few facts about budgets:
  • The ledger is shared by every model and tool node of the execution (and agent), and maxCalls counts tool calls only.
  • The check runs before a call and the usage is added after it, so one call can overshoot, and calls that run at the same time can each pass the check.
  • Budgets live in server memory. They start again from zero after a restart or a resume.
Text from users, web pages or tools goes straight into the prompt. A model can be talked into asking for a tool you did not expect. Keep allowedTools narrow, put requireApproval on tools that change things, and never let a tool name or argument from a model reach something dangerous without a check.