> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modulex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent node

> Run an autonomous agent step inside a workflow: bind integration tools to a model and let it call them in a manual loop until it produces a final answer. Full AgentNodeConfig reference, the tool loop and stop conditions, HITL, credit impact, inputs/outputs, errors, and a worked example.

export const MediaEmbed = ({id, type = 'screenshot', caption = '', ext, ratio = '16 / 9'}) => {
  const isVideo = type === 'video' || type === 'app_video';
  const resolvedExt = ext || (isVideo ? 'mp4' : type === 'screenshot' ? 'webp' : 'svg');
  const src = 'https://media.modulex.dev/' + id + '.' + resolvedExt;
  const [status, setStatus] = useState('loading');
  const [isDev, setIsDev] = useState(false);
  const [inView, setInView] = useState(false);
  const boxRef = useRef(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const h = window.location.hostname;
    setIsDev(h === 'localhost' || h === '127.0.0.1' || h.endsWith('.mintlify.app'));
  }, []);
  useEffect(() => {
    if (inView) return;
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true);
      return;
    }
    const el = boxRef.current;
    if (!el) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) {
        setInView(true);
        io.disconnect();
      }
    }, {
      rootMargin: '300px'
    });
    io.observe(el);
    return () => io.disconnect();
  }, [inView]);
  if (status === 'missing') {
    if (!isDev) return null;
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      gap: '0.4rem',
      padding: '1rem 1.25rem',
      margin: '1.25rem 0',
      width: '100%',
      aspectRatio: ratio,
      boxSizing: 'border-box',
      border: '1px dashed rgba(128,128,128,0.45)',
      borderRadius: '0.75rem',
      background: 'rgba(128,128,128,0.06)',
      color: 'currentColor',
      fontSize: '0.85rem',
      lineHeight: 1.45
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      opacity: 0.75
    }}>
          <span aria-hidden="true">🎬</span>
          <code style={{
      fontSize: '0.75rem'
    }}>{id}</code>
          <span style={{
      fontSize: '0.65rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      padding: '0.1rem 0.4rem',
      borderRadius: '0.4rem',
      background: 'rgba(128,128,128,0.18)'
    }}>
            {type}
          </span>
        </div>
        <div style={{
      opacity: 0.9
    }}>{caption || 'Media not uploaded yet.'}</div>
        <div style={{
      fontSize: '0.7rem',
      opacity: 0.5
    }}>
          Upload to R2 as <code>{id}.{resolvedExt}</code> — preview only, hidden in production.
        </div>
      </div>;
  }
  const mediaStyle = {
    display: status === 'loaded' ? 'block' : 'none',
    width: '100%',
    height: 'auto',
    borderRadius: '0.75rem'
  };
  const media = isVideo ? <video src={inView ? src : undefined} autoPlay loop muted playsInline preload="metadata" onLoadedData={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} /> : <img src={inView ? src : undefined} alt={caption} onLoad={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} />;
  return <figure style={{
    margin: '1.25rem 0'
  }}>
      <div ref={boxRef} style={status === 'loaded' ? {
    width: '100%'
  } : {
    width: '100%',
    aspectRatio: ratio,
    borderRadius: '0.75rem',
    background: 'rgba(128,128,128,0.06)'
  }}>
        {media}
      </div>
      {status === 'loaded' && caption ? <figcaption style={{
    marginTop: '0.5rem',
    textAlign: 'center',
    fontSize: '0.85rem',
    opacity: 0.7
  }}>
          {caption}
        </figcaption> : null}
    </figure>;
};

The `agent` node runs a language model that can call integration tools and loop until it reaches a final answer, all inside a single workflow step. Where the [LLM node](/workflow-builder/nodes/llm) makes exactly one model call, the `agent` node binds a set of tools to the model and repeatedly lets the model decide whether to call a tool or finish — a manual tool-calling loop bounded by `max_iterations`.

Use it when a step needs to gather information or act through tools before answering — for example "find the three most relevant GitHub issues and summarize them" or "search the web, then draft a reply." When you want deterministic control over a single tool call instead, use the [tool node](/workflow-builder/nodes/tool); when you want generation with no tools, use the [LLM node](/workflow-builder/nodes/llm).

<Note>
  The `agent` node is the in-workflow agentic step. It is distinct from the standalone [Assistant](/assistant/overview), which is a workflow-independent agentic chat. They share the same idea — a model that loops over tools — but the `agent` node runs as one node in a compiled workflow graph and writes its result into run state, while the Assistant is its own chat surface.
</Note>

The result of the loop is written into run state under the node's own `id`, the same convention every node follows. To pass data into the agent's prompt and tools, use `{{node_id.field}}` references — the same reference system documented in [variables & references](/workflow-builder/variables-and-references) and [the workflow engine](/concepts/workflow-engine).

<MediaEmbed id="MX-MEDIA-3080" type="screenshot" caption={"the agent node configuration panel in the workflow builder"} />

## What the node does

When the engine compiles your workflow, the `agent` node is turned into a single async step ([workflow engine](/concepts/workflow-engine)). At run time the node:

1. Initializes the model from `llm` and binds the configured `tools` to it (so the model can emit tool calls). If `tools` is empty, the model runs with no tools bound — effectively a one-shot LLM call inside the loop.
2. Resolves `system_prompt` against the current run state, replacing every `{{...}}` token with its resolved value.
3. Builds the user message from the first of these that is set: `prompt_template` (deprecated), then `input_mapping`, then `input_keys` (deprecated), then the run-state `input` field. The agent's user message must not be empty (see [errors](#errors)).
4. Enters the tool loop, up to `max_iterations` times:
   * Calls the model with the running message list.
   * Records token usage for billing (managed usage only — see [credit impact](#credit-impact)).
   * If the model returns no tool calls, the loop ends and the model's content becomes the result.
   * If the model returns tool calls, the node executes each one, appends the tool result as a tool message, and loops again.
5. Writes the final content into run state under the node's `id`.

<MediaEmbed id="MX-MEDIA-3081" type="image" caption={"the agent node manual tool loop"} />

### The manual tool loop

The agent node runs its own loop rather than delegating to a framework agent executor. Each iteration is one model call plus zero or more tool executions:

<Steps>
  <Step title="Model call">
    The model is invoked with the current message list (system message, the user message, and any tool messages from previous iterations). Token usage is recorded after every call.
  </Step>

  <Step title="Tool-call check">
    If the response contains no tool calls, the loop stops and the response content is the node's result. If it contains tool calls, each call is executed in turn.
  </Step>

  <Step title="Tool execution">
    For each tool call, the node looks up the bound tool by name, merges in any `parameter_defaults` for that tool (only when the model did not already supply that argument), and invokes the tool. The tool's output is appended to the message list as a tool message so the model sees it on the next turn. A tool that raises is caught: the error text is appended as the tool message (prefixed `Error:`) so the model can react, rather than failing the node.
  </Step>

  <Step title="Loop or finish">
    The loop repeats from the model call until the model returns no tool calls, or until `max_iterations` is reached.
  </Step>
</Steps>

If the model's final content looks like a JSON object (it starts with `{`), the node parses it into an object; otherwise the content is stored as a string.

### Stop conditions

The loop ends on exactly one of these:

* **The model returns no tool calls.** This is the normal finish: the model's content becomes the result.
* **`max_iterations` is reached.** The loop stops even if the model still wanted to call a tool. The result is the content of the last message in the conversation — which may be a partial answer or a tool result rather than a clean final response. Size `max_iterations` to the task (see the field reference below).

<Warning>
  Reaching `max_iterations` is not an error — the node still returns and the run continues. But the returned value may be incomplete because the model was mid-task. If your downstream logic depends on a finished answer, raise `max_iterations`, simplify the task, or validate the output with a [guardrails node](/workflow-builder/nodes/guardrails).
</Warning>

## Configuration (`AgentNodeConfig`)

These fields live on the node's `agent_config`. In the builder you set them through the detail panel; over the API they appear inside the node definition (the builder also accepts a wrapped `{config: {...}}` form, which the backend normalizes to `agent_config`).

<ParamField path="llm" type="LLMConfig object" required>
  The model the agent reasons with and calls tools through. Required. See [the `llm` object](#the-llm-object) below for its fields. This selects both the provider (managed or BYOK) and the specific model. The model must support tool calling for `tools` to be useful.
</ParamField>

<ParamField path="tools" type="ToolDefinition[]" default="[]">
  The integration tools available to the agent. Optional; defaults to an empty list. Each entry is a [`ToolDefinition`](#the-tool-objects) identifying an integration and action. The agent decides when and how to call them. With no tools, the node behaves like a single LLM call wrapped in the loop. See [using integration tools](/integrations/overview) and the [tool node](/workflow-builder/nodes/tool) for the same `ToolDefinition` shape.
</ParamField>

<ParamField path="system_prompt" type="string" required>
  The system message that sets the agent's role, goal, and how it should use its tools. **Required** — unlike the LLM node, the agent node requires a system prompt. Supports `{{node_id.field}}` and `{{input}}` references. Write it to tell the model what tools it has and when to stop.
</ParamField>

<ParamField path="user_prompt" type="string">
  The human message — the actual task. Optional. Supports `{{node_id.field}}` and `{{input}}` references. The agent builds its user message from the first source that is set, in this order: `prompt_template` (deprecated), `input_mapping`, `input_keys` (deprecated), then the run-state `input` field. Provide a `user_prompt` or an `input_mapping` so the agent has a task; an empty user message raises an error.
</ParamField>

<ParamField path="input_mapping" type="object" default="{}">
  A map of named inputs resolved from run state with `{{node_id.field}}` references. Optional. When the agent has no `prompt_template`, the resolved mapping becomes the user message: a single entry is passed as its value; multiple entries are formatted as `key: value` lines. `None` and empty-string values are skipped during resolution. Use this to feed structured upstream data into the agent.
</ParamField>

<ParamField path="max_iterations" type="integer" default="10">
  The maximum number of model calls in the tool loop. Optional; defaults to `10`. Each model call is one iteration; a turn that triggers tool calls and loops counts as one iteration. The loop stops when the model returns no tool calls or when this limit is hit. Higher values let the agent take more steps but cost more (every iteration is a billable model call for managed models).
</ParamField>

<Accordion title="Deprecated fields — do not use in new workflows">
  These fields exist on `AgentNodeConfig` for backward compatibility and are normalized away or ignored. Author new workflows without them.

  <ParamField path="prompt_template" type="string" deprecated>
    Deprecated. Use `user_prompt` instead. If `prompt_template` is set and `user_prompt` is not, the backend copies `prompt_template` into `user_prompt` automatically. When present, `prompt_template` takes priority as the agent's user message.
  </ParamField>

  <ParamField path="input_keys" type="string[]" deprecated>
    Deprecated. Use `input_mapping` (or `{{node_id.field}}` references in `user_prompt`) instead of listing state keys. When used, the listed state values are concatenated with spaces into the user message.
  </ParamField>

  <ParamField path="output_key" type="string" deprecated>
    Deprecated. Output is always stored under the node `id`. This field is ignored for routing.
  </ParamField>
</Accordion>

### The `llm` object

The `llm` field is an `LLMConfig`. It is required and identifies the provider, the model, and (optionally) which stored credential to use. It is the same object the [LLM node](/workflow-builder/nodes/llm) uses.

<ParamField path="llm.integration_name" type="string" required>
  The provider integration on the wire. Required. Use `modulexai` for ModuleX-managed models (billed in credits). For BYOK, use the provider integration name — for example `anthropic`, `openai`, `gemini`, or `xai`. See [LLM providers](/integrations/llm-providers/overview) and [managed vs BYOK](#managed-vs-byok).
</ParamField>

<ParamField path="llm.provider_id" type="string" required>
  The underlying provider id. Required. For example `anthropic`, `openai`, `gemini`, `xai`, or — for managed routing — `openrouter`. Each model in a provider catalog declares its own `provider_id`; match it for the model you pick.
</ParamField>

<ParamField path="llm.model_id" type="string" required>
  The standardized model id, for example `claude-sonnet-4.6`, `claude-haiku-4.5`, or `gpt-5.4-mini`. Required. ModuleX maps this id to the provider's served model. Pick a model that supports tool calling. Models marked deprecated or in maintenance are automatically routed to their replacement for managed integrations. Browse available ids on each [provider page](/integrations/llm-providers/overview).
</ParamField>

<ParamField path="llm.temperature" type="number" default="0.4">
  Sampling temperature passed to the model. Optional; defaults to `0.4`. Lower values make the agent's decisions more deterministic; higher values make them more varied.
</ParamField>

<ParamField path="llm.credential_id" type="string">
  A specific stored credential to use for the model calls. Optional. If omitted, ModuleX resolves a credential for the integration in the current organization. Required in practice for BYOK providers (you must have connected your own key). See [managing credentials](/integrations/managing-credentials).
</ParamField>

### The tool objects

Each entry in `tools` is a `ToolDefinition`. The same shape is used by the [tool node](/workflow-builder/nodes/tool), with one key difference: the agent node uses `parameter_defaults` only and **ignores `parameter_overrides`**. The agent chooses arguments autonomously; defaults fill in only what the model did not supply. If you need to force exact argument values, use a [tool node](/workflow-builder/nodes/tool) instead.

<ParamField path="tools[].integration_name" type="string" required>
  The integration the tool belongs to, for example `tavily` or `github`. Required. See the [integrations overview](/integrations/overview) and the [catalog](/integrations/catalog).
</ParamField>

<ParamField path="tools[].service_name" type="string" required>
  The action/tool within the integration, for example `web_search` or `create_issue`. Required.
</ParamField>

<ParamField path="tools[].credential_id" type="string">
  A specific stored credential to use for this tool. Optional. If omitted, ModuleX resolves a credential for the integration in the current organization. See [managing credentials](/integrations/managing-credentials).
</ParamField>

<ParamField path="tools[].parameter_defaults" type="object">
  Default argument values for the tool, applied only when the model does not supply that argument. Optional. Supports `{{node_id.field}}` references, which are resolved against run state before the tool is called. This is the only parameter mechanism the agent node honors.
</ParamField>

<ParamField path="tools[].parameter_overrides" type="object" deprecated>
  Ignored by the agent node. Optional and present only because the `ToolDefinition` shape is shared with the [tool node](/workflow-builder/nodes/tool), which does honor overrides. To force argument values in a workflow, use a tool node.
</ParamField>

### Retry configuration

The `agent` node is retry-wrapped at the node level. You can attach a `retry_config` to the node definition itself (not inside `agent_config`) to control how a failed node execution is retried. If you omit it, the engine applies its default retry policy (2 retries). Errors are only retried when their type is in `retry_on_error_types`.

<ParamField path="retry_config.max_attempts" type="integer" default="3">
  Total attempts including the first. Range 1–10. `1` means no retry; `3` means the initial attempt plus 2 retries.
</ParamField>

<ParamField path="retry_config.initial_interval" type="number" default="1.0">
  Seconds to wait before the first retry. Range 0.1–60.
</ParamField>

<ParamField path="retry_config.backoff_factor" type="number" default="2.0">
  Multiplier applied to the delay between successive retries (exponential backoff). Range 1–5.
</ParamField>

<ParamField path="retry_config.retry_on_error_types" type="string[]" default="[&#x22;TimeoutError&#x22;, &#x22;ConnectionError&#x22;, &#x22;HTTPError&#x22;]">
  Exception type names that trigger a retry. Errors not in this list fail immediately. A credit-exhaustion stop is not retried.
</ParamField>

<Note>
  Retry applies to the whole node execution, not to a single tool call inside the loop. A tool that raises mid-loop is caught and reported back to the model as a tool message (prefixed `Error:`) instead of failing the node — so a single bad tool call does not trigger a node-level retry. See [error handling & retries](/workflow-builder/error-handling-retries).
</Note>

## Inputs and outputs

### Inputs

The agent node has no fixed input fields. It reads whatever you reference in `system_prompt`, `user_prompt`/`input_mapping`, and each tool's `parameter_defaults` from run state:

* `{{node_id}}` — the entire output of an upstream node (typed value: string, object, or array).
* `{{node_id.field}}` — a nested value via dot/bracket path, for example `{{search_1.results[0].url}}`. An out-of-range index or missing key resolves to nothing.
* `{{input}}` — the run's top-level input field.

A reference that is the entire string keeps its native type; a reference embedded inside other text is string-substituted (objects/arrays are JSON-encoded). During `input_mapping` resolution, `None` and empty-string values are skipped. See [variables & references](/workflow-builder/variables-and-references) for the full resolution model.

### Outputs

The node writes one value into run state under its `id`:

<ResponseField name="{node_id}" type="string | object">
  The agent's final content.

  <Expandable title="How the value is shaped">
    <ResponseField name="text" type="string">
      When the final model content is plain text, the value is that string.
    </ResponseField>

    <ResponseField name="parsed object" type="object">
      When the final content starts with `{` and parses as JSON, the node stores the parsed object. Otherwise the raw string is stored.
    </ResponseField>

    <ResponseField name="max-iterations stop" type="string | object">
      When the loop hits `max_iterations`, the value is the content of the last message in the conversation (a partial answer or a tool result), parsed as JSON if it looks like an object. Validate downstream — see the warning under [stop conditions](#stop-conditions).
    </ResponseField>
  </Expandable>
</ResponseField>

Downstream nodes read this with `{{node_id}}` (whole value) or `{{node_id.field}}` (a field of a parsed object).

<Note>
  Intermediate tool calls and tool results live only inside the agent's loop; they are not written to run state as separate fields. Only the final content is stored under the node `id`. If a downstream step needs a specific tool result deterministically, run that tool as a separate [tool node](/workflow-builder/nodes/tool) instead.
</Note>

## Streaming

An agent node streams at the **node level** over [SSE](/realtime/sse-streaming), like other workflow nodes. The model's per-iteration tool calls and tool results are not streamed as separate run events; you receive one `node_update` when the node finishes its loop, carrying the node's final output. On the wire this event is flat and uses the keys `node` and `output` — for example `{type, node, output}` — not the typed model field names. The run then continues to the next node and ends with a `done` event.

<Note>
  The realtime wire dicts differ from the typed event models: the `node_update` frame uses `node` and `output`, and `done` carries only a `message`. Parse each SSE frame as JSON and switch on its `type` field — there is no SSE `event:` line. Details on [SSE run streaming](/realtime/sse-streaming).
</Note>

## Human-in-the-loop

The agent node itself does not pause for human input — it runs its tool loop to completion (or to `max_iterations`) and returns. To add a human approval or input step around an agent in a workflow, use a separate [interrupt node](/workflow-builder/nodes/interrupt), which pauses the run with a structured question and resumes after a person responds. The typical pattern is: agent node produces a draft, interrupt node asks "approve this?", then a [tool node](/workflow-builder/nodes/tool) acts on the approved result.

<Note>
  HITL inside the agentic loop — where the model itself can ask the user a question or request a credential mid-task — is a feature of the standalone [Assistant](/assistant/overview), not the in-workflow agent node. For pause/resume semantics in workflows, see the [interrupt node](/workflow-builder/nodes/interrupt) and [human-in-the-loop resume](/realtime/hitl).
</Note>

## Managed vs BYOK

The `llm.integration_name` you choose decides who runs the model and how it is billed. Tool execution itself is not a model cost — the model billing follows the same rules as the [LLM node](/workflow-builder/nodes/llm):

<CardGroup cols={2}>
  <Card title="Managed (modulexai)" icon="server">
    Set `integration_name` to `modulexai`. The model calls run through ModuleX-provisioned providers and are **billed in credits** by input/output tokens — once per iteration. No provider key of your own is required. See [ModuleX-managed models](/integrations/llm-providers/modulexai).
  </Card>

  <Card title="BYOK (your own key)" icon="key">
    Set `integration_name` to the provider (for example `anthropic`, `openai`, `gemini`, `xai`) and connect your own credential. Model usage is billed directly by that provider with **no ModuleX markup** and is **not** charged in credits (token usage is recorded for analytics only). See [LLM providers](/integrations/llm-providers/overview) and [managing credentials](/integrations/managing-credentials).
  </Card>
</CardGroup>

## Credit impact

There is no fixed per-node credit charge for an agent node. Charging is metered per model call, and the agent makes one model call per loop iteration:

* **Managed (`modulexai`)** — each iteration's model call records token usage and charges credits for input and output tokens. An agent that loops `N` times incurs `N` billable model calls, so a higher `max_iterations` (or a task that takes many tool steps) costs more. ModuleX measures usage in [credits](/billing/credits) (the managed-usage billing unit).
* **BYOK** — token usage is recorded for analytics, but **no credits are charged**; you pay your provider directly.
* **Tools the agent calls** — an integration tool may have its own cost depending on the provider, but tool execution is not metered as ModuleX managed-model credits here. A managed-knowledge retrieval reached through a [knowledge node](/workflow-builder/nodes/knowledge) is billed separately; see [credits & metering](/billing/credits).

Workflow runs go through the [usage gate](/billing/usage-gating), which admits the run before it starts. The gate is best-effort once a run is executing: a billing problem mid-run surfaces as a node error rather than crashing the run silently. If an organization runs out of credits mid-run, the node's error event carries a stable reason token (`credit_exhausted`) so clients can detect a budget stop deterministically. See [usage gating & limits](/billing/usage-gating) and [credits & metering](/billing/credits).

<Note>
  Workflow run, [Composer](/workflow-builder/composer), [Assistant](/assistant/overview), and managed-knowledge surfaces are gated by the billing admission gate, which can return a `DenialEnvelope` as **402 / 403 / 429**. The flat envelope shape is `{code, layer, key, current, limit, reason}`. See [errors & status codes](/api-reference/errors) and [usage gating](/billing/usage-gating).
</Note>

## Errors

The agent node surfaces failures through the run's node-error event after node-level retries are exhausted. Common cases:

<Accordion title="Missing or invalid agent_config">
  If an `agent` node has no `agent_config`, compilation fails with a configuration error (`Agent node <id> missing agent_config`). Ensure `llm`, `system_prompt`, and (usually) `tools` are set.
</Accordion>

<Accordion title="Empty agent input">
  If none of `prompt_template`, `input_mapping`, `input_keys`, or the run-state `input` field resolves to non-empty text, the node raises with a message like `Agent node '<id>' has empty input. Please provide prompt_template or input_mapping in agent_config.` Provide a `user_prompt` or an `input_mapping` so the agent has a task.
</Accordion>

<Accordion title="A tool raises during the loop">
  A tool that throws is **not** a node failure: the node catches the exception and appends an error tool message (prefixed `Error:`) so the model can adjust on the next iteration. The node still returns a result. If a tool consistently fails and the model cannot recover, the agent may return a partial or apologetic answer — validate downstream, or move the call to a dedicated [tool node](/workflow-builder/nodes/tool) where a failure surfaces as a node error.
</Accordion>

<Accordion title="Provider / network errors">
  Timeouts, connection failures, and HTTP errors from the model provider are retried per `retry_config` (defaults: `TimeoutError`, `ConnectionError`, `HTTPError`). When retries are exhausted, the node emits a `node_error` event with `error_type`, `error_message`, the attempt count, and `recoverable: false`, then the run fails. See [error handling & retries](/workflow-builder/error-handling-retries).
</Accordion>

<Accordion title="Credit exhaustion mid-run (managed)">
  If a managed model call cannot be billed because credits are exhausted, the node's `node_error` event includes a stable `reason` token, `credit_exhausted`, so a client can match it without parsing free text. Resolve by topping up the [wallet](/billing/wallet) or upgrading your [plan](/billing/plans).
</Accordion>

<Accordion title="Max iterations reached without a final answer">
  This is not raised as an error — the node returns the last message content (see [stop conditions](#stop-conditions)). If you need to treat an unfinished agent as a failure, check the output downstream with a [conditional node](/workflow-builder/nodes/conditional) or a [guardrails node](/workflow-builder/nodes/guardrails).
</Accordion>

For the full taxonomy of error-envelope shapes and which surface emits each, see [errors & status codes](/api-reference/errors).

## Full example

A research agent that searches the web and reads GitHub issues, then writes a short brief. The first tab shows the agent node definition as it appears in a workflow over the API; the next tabs run the workflow end to end with the SDKs.

<CodeGroup>
  ```json Agent node definition theme={null}
  {
    "id": "research_1",
    "type": "agent",
    "name": "Research agent",
    "x": 480,
    "y": 160,
    "retry_config": {
      "max_attempts": 3,
      "initial_interval": 1.0,
      "backoff_factor": 2.0
    },
    "agent_config": {
      "llm": {
        "integration_name": "modulexai",
        "provider_id": "openrouter",
        "model_id": "claude-sonnet-4.6",
        "temperature": 0.2
      },
      "system_prompt": "You are a research assistant. Use the web_search tool to find recent sources and the github list_issues tool to inspect the repository. Stop and write a concise brief once you have enough to answer. Return JSON with keys summary and sources.",
      "user_prompt": "Research the current state of: {{intake_1.topic}}",
      "tools": [
        {
          "integration_name": "tavily",
          "service_name": "web_search",
          "parameter_defaults": { "max_results": 5 }
        },
        {
          "integration_name": "github",
          "service_name": "list_issues",
          "parameter_defaults": { "repo": "{{intake_1.repo}}", "state": "open" }
        }
      ],
      "max_iterations": 8
    }
  }
  ```

  ```bash cURL theme={null}
  # Run a workflow that contains the research_1 agent node
  curl -X POST https://api.modulex.dev/workflows/run \
    -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \
    -H "X-Organization-ID: org_4d2b18" \
    -H "Content-Type: application/json" \
    -d '{
          "workflow_id": "wf_7f3a9c",
          "input": {
            "intake_1": {
              "topic": "trends in retrieval-augmented generation",
              "repo": "modulex/modulex"
            }
          }
        }'
  ```

  ```python Python theme={null}
  from modulex import ModuleX

  client = ModuleX(
      api_key="mx_live_xxxxxxxxxxxxxxxxxxxx",
      organization_id="org_4d2b18",
  )

  run = client.workflows.run(
      "wf_7f3a9c",
      input={
          "intake_1": {
              "topic": "trends in retrieval-augmented generation",
              "repo": "modulex/modulex",
          },
      },
  )

  # research_1 wrote its final content under its node id.
  # The system prompt asked for JSON, so the agent's output parses as an object.
  print(run.state["research_1"]["summary"])
  print(run.state["research_1"]["sources"])
  ```

  ```javascript JavaScript theme={null}
  import { ModuleX } from "@modulex/sdk";

  const client = new ModuleX({
    apiKey: "mx_live_xxxxxxxxxxxxxxxxxxxx",
    organizationId: "org_4d2b18",
  });

  const run = await client.workflows.run("wf_7f3a9c", {
    input: {
      intake_1: {
        topic: "trends in retrieval-augmented generation",
        repo: "modulex/modulex",
      },
    },
  });

  // research_1 wrote its final content under its node id.
  // The system prompt asked for JSON, so the agent's output parses as an object.
  console.log(run.state.research_1.summary);
  console.log(run.state.research_1.sources);
  ```
</CodeGroup>

Every request authenticates with `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. See [authentication](/api-reference/authentication) and the [run-a-workflow guide](/guides/run-a-workflow). To stream the run instead of waiting for the final state, see [SSE run streaming](/realtime/sse-streaming) and [streaming & HITL in the SDKs](/sdks/streaming-hitl).

<Note>
  The agent's output shape depends on the model and your prompt. Asking for JSON in `system_prompt` makes the result parse into an object (so `{{research_1.summary}}` resolves), but it is not guaranteed — the model may return prose. Validate the shape with a [guardrails node](/workflow-builder/nodes/guardrails) when downstream logic depends on it.
</Note>

## Related

<CardGroup cols={2}>
  <Card title="LLM node" icon="sparkles" href="/workflow-builder/nodes/llm">
    Generate once with no tools, including structured JSON output.
  </Card>

  <Card title="Tool node" icon="wrench" href="/workflow-builder/nodes/tool">
    Call a single integration tool deterministically, with parameter overrides.
  </Card>

  <Card title="Interrupt node" icon="hand" href="/workflow-builder/nodes/interrupt">
    Pause a run to ask a person for approval or input, then resume.
  </Card>

  <Card title="Assistant" icon="bot" href="/assistant/overview">
    The standalone agentic chat — agentic loop with HITL, no workflow required.
  </Card>

  <Card title="Variables & references" icon="braces" href="/workflow-builder/variables-and-references">
    How `{{node_id.field}}` references resolve against run state.
  </Card>

  <Card title="Node types overview" icon="diagram-project" href="/workflow-builder/nodes/overview">
    All nine node types and how each writes its result into run state.
  </Card>
</CardGroup>
