> ## 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.

# Node types reference

> The nine ModuleX node types, how each writes its result into run state under its own id, the {{node_id.field}} reference system, and how to choose the right node for a step.

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>;
};

A node is one step in a workflow graph. When a run executes, each node receives the current run [state](/concepts/workflow-engine), does its work, and writes its result back into state. ModuleX supports exactly **nine node types**. This page covers all nine, the single convention that ties them together — every node writes its result to state under its own `id` — and how later nodes read earlier results with `{{node_id.field}}` references.

For the engine that compiles and runs these nodes (the state graph, the run state, edges, and loops), see [Workflow engine & nodes](/concepts/workflow-engine). For the cross-node data flow in depth, see [Variables & references](/workflow-builder/variables-and-references).

<MediaEmbed id="MX-MEDIA-3060" type="image" caption={"Diagram of the nine node types arranged on a small canvas, with arrows showing how each node's output flows into the run state and is read by a later node via a `{{node_id.field}}` reference label."} />

## The nine node types

These are the canonical `NodeType` values. The string in the first column is the exact `type` you set on a node — there is no other set, and an unknown value raises `Unsupported node type`.

| `type`        | Node                                                    | What it does                                                                                         | Category |
| ------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------- |
| `llm`         | [LLM node](/workflow-builder/nodes/llm)                 | Call a language model with a system/user prompt; optionally enforce a structured-output JSON schema. | Model    |
| `agent`       | [Agent node](/workflow-builder/nodes/agent)             | Run an autonomous model-plus-tools loop that decides which tools to call, up to `max_iterations`.    | Model    |
| `tool`        | [Tool node](/workflow-builder/nodes/tool)               | Call one integration tool deterministically with a mapped input.                                     | Action   |
| `knowledge`   | [Knowledge node](/workflow-builder/nodes/knowledge)     | Retrieve relevant chunks from a knowledge base (managed `modulexdb` or BYOK vector store) for RAG.   | Data     |
| `function`    | [Function node](/workflow-builder/nodes/function)       | Run one of the built-in functions: HTTP request, webhook, or schema validation.                      | Action   |
| `transformer` | [Transformer node](/workflow-builder/nodes/transformer) | Reshape, map, filter, and convert data between steps with a pipeline of operations.                  | Data     |
| `conditional` | [Conditional node](/workflow-builder/nodes/conditional) | Branch on an expression or an LLM decision, or drive a `for` / `foreach` / `while` loop.             | Control  |
| `guardrails`  | [Guardrails node](/workflow-builder/nodes/guardrails)   | Validate content with JSON-schema, regex, and PII checks, then block, warn, transform, or route.     | Control  |
| `interrupt`   | [Interrupt node](/workflow-builder/nodes/interrupt)     | Pause the run to ask a human a structured question, then resume with their answer.                   | Control  |

<Card title="Pick a node by intent" icon="signpost" horizontal>
  Use the [decision guide](#how-to-pick-a-node) below if you are not sure which type fits a step. Each node's own page documents every parameter, input, output, error, and credit impact in full.
</Card>

## The output-to-state convention

Every node writes its result into the run state under a key equal to its own node `id`. There is no separate "output name" to wire up — the node id *is* the output key.

If a node with `id: "node_summary"` produces the string `"Quarterly revenue rose 12%."`, then after that node runs the state contains:

```json theme={null}
{
  "node_summary": "Quarterly revenue rose 12%."
}
```

This is enforced by the engine in two ways:

* When the graph is compiled, **one state field is added per node id** (typed `Any`) so each node's output streams back into state correctly.
* Each node's executor returns a single-key dict of the form `{node_id: result}`. The node id is the only output key.

<Warning>
  Some node configs still expose an `output_key` field (and a few expose `input_keys`). Both are **deprecated** and ignored — output is always stored under the node id. Do not set them. Use `{{node_id.field}}` references (below) instead of `input_keys`.
</Warning>

The shape of `result` depends on the node type — a string for a simple [LLM node](/workflow-builder/nodes/llm), an object for a [Tool node](/workflow-builder/nodes/tool), a list of chunks plus context for a [Knowledge node](/workflow-builder/nodes/knowledge), and so on. Each node page documents its exact result shape under "Output". A few nodes write a structured envelope rather than a bare value, which you reference by sub-path:

| Node type     | Result written under the node id                                                                                                                                       |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `llm`         | The model's content (a string, or your structured-output object).                                                                                                      |
| `tool`        | The tool's unwrapped result object (a `{success, action, result}` wrapper is flattened to the root, with `result` kept for backward-compatible `.result.field` paths). |
| `agent`       | The agent's final content after its tool loop.                                                                                                                         |
| `function`    | The function's `data` on success; on failure an object like `{error, success: false}`.                                                                                 |
| `knowledge`   | An object `{total_results, chunks, context}` (shape depends on `output_format`).                                                                                       |
| `conditional` | For expression branching, `{matched_target}`; for LLM routing, `{llm_routing_decision}`.                                                                               |
| `guardrails`  | A validation envelope `{valid, validations, original_data, transformed_data, ...}`.                                                                                    |
| `interrupt`   | The human's resume value, once the run resumes.                                                                                                                        |
| `transformer` | The transformed value after all operations run.                                                                                                                        |

## Referencing earlier results: `{{node_id.field}}`

Later nodes read earlier results with the template syntax `{{node_id.path}}`. This is the single mechanism for passing data between nodes — there are no explicit input wires beyond the graph edges.

How resolution behaves:

* A value that is **exactly** one reference (for example `{{node_search.results}}`) resolves to the **native typed value** — a list stays a list, a number stays a number.
* A reference **inside a larger string** (for example `Summarize: {{node_search.title}}`) is substituted into the string; non-string values are JSON-encoded into the text.
* The path supports **dot and bracket access**: `{{node_abc.results[0].title}}` reads the `title` of the first item of `node_abc`'s `results`.
* An unresolved or out-of-range path resolves to `null` (and an unresolved reference left in a template string is kept intact, not blanked).
* In a list, an item written as `{{...node_id.path}}` (note the leading `...`) is a **spread**: if it resolves to a list, its items are spliced into the surrounding list; a scalar or object is appended; `null` contributes nothing.

You can use references anywhere a config field accepts a template: prompts, tool input mappings, conditional expressions, knowledge queries, transformer sources, and interrupt messages. Each node page marks which of its fields accept `{{...}}`.

```text title="Reference patterns" theme={null}
{{node_llm_1}}                       Whole output of node_llm_1
{{node_search.results[0].url}}       Nested path with array index
Subject: {{node_meta.title}}         Reference embedded in a string
{{input}}                            The run's top-level input value
{{...node_split.items}}             Spread a list into a surrounding list
```

<Note>
  The same `{{node_id.field}}` syntax is what the [AI Composer](/workflow-builder/composer) writes when it wires nodes together for you, and what the canvas shows in field pickers. See [Variables & references](/workflow-builder/variables-and-references) for the full resolution rules, reducers, and the run `state` model.
</Note>

## What every node shares

These fields exist on every node regardless of type. They are set on the node itself, not inside the type-specific config.

<ParamField path="id" type="string" required>
  Unique node identifier. This is also the **key its output is written under** in run state (see the output-to-state convention above). Referenced elsewhere as `{{id.field}}`.
</ParamField>

<ParamField path="type" type="string" required>
  One of the nine node types: `llm`, `tool`, `agent`, `function`, `conditional`, `interrupt`, `transformer`, `guardrails`, `knowledge`. Any other value raises `Unsupported node type` at compile time.
</ParamField>

<ParamField path="name" type="string">
  Human-readable label shown on the canvas. Optional.
</ParamField>

<ParamField path="description" type="string">
  What the node does. Optional, display only.
</ParamField>

<ParamField path="enabled" type="boolean" default="true">
  Whether the node runs. A disabled node is **skipped at execution time** and its edges are rewired to the next enabled target, but it remains visible in the workflow definition. `null` is treated as `true`.
</ParamField>

<ParamField path="retry_config" type="object">
  Per-node retry policy. Applies to every node type **except `interrupt`**, which is never retry-wrapped (pausing for a human is not a retriable failure). See the fields below.
</ParamField>

<Expandable title="retry_config fields">
  <ParamField path="retry_config.max_attempts" type="integer" default="3">
    Total attempts including the first (`1` = no retry). Range 1–10.
  </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">
    Exponential backoff multiplier applied between retries. Range 1.0–5.0.
  </ParamField>

  <ParamField path="retry_config.retry_on_error_types" type="string[]" default="[&#x22;TimeoutError&#x22;, &#x22;ConnectionError&#x22;, &#x22;HTTPError&#x22;]">
    Error class names that trigger a retry. Errors not in this list fail the node immediately.
  </ParamField>
</Expandable>

Each type then carries one type-specific config object — `llm_config`, `tool_config`, `agent_config`, `function_config`, `conditional_config`, `interrupt_config`, `transformer_config`, `guardrails_config`, or `knowledge_config` — documented in full on that node's page. (The builder also accepts a wrapped `{config: {...}}` form and normalizes it to the type-specific key.)

## How a node fails

When a node raises after exhausting its retries, the run emits a `node_error` event and the run stops. The error carries a machine-readable `reason` — for example `credit_exhausted` when the [billing gate](/billing/usage-gating) stops a managed call mid-run — so a client can match it deterministically. See [Error handling & retries](/workflow-builder/error-handling-retries) for retry behavior and debugging, and [Errors & status codes](/api-reference/errors) for the envelope shapes returned by the run surface.

## Credit impact

There is **no flat per-node credit charge**. Credits are metered inline based on what a node actually does:

* **`llm` and `agent` nodes**, and the LLM call inside a `conditional` node's LLM routing, record [credit](/billing/credits) usage per call from the model's input and output tokens. The agent node records usage on **every iteration** of its loop.
* **`knowledge` nodes** consume credits **only** when retrieving from the managed [`modulexdb`](/integrations/knowledge-providers/modulexdb) store — a retrieval base plus the query-embedding cost. Bring-your-own-vector-store providers ([Qdrant](/integrations/knowledge-providers/qdrant), [Pinecone](/integrations/knowledge-providers/pinecone), [Weaviate](/integrations/knowledge-providers/weaviate), [MongoDB Atlas](/integrations/knowledge-providers/mongodb-atlas)) are not metered by ModuleX.
* **`tool`, `function`, `transformer`, `conditional` (expression), `guardrails`, and `interrupt` nodes** do not themselves consume credits. A `tool` node may incur cost at the connected provider, but ModuleX does not meter it.

For when the [billing gate](/billing/usage-gating) can stop a run and what surfaces it applies to, see [Credits & the billing model](/concepts/credits-billing).

## How to pick a node

<AccordionGroup>
  <Accordion title="I need to call a language model" icon="message-bot">
    Use an [LLM node](/workflow-builder/nodes/llm) for a single, deterministic model call — one prompt in, one answer out, optionally constrained to a JSON schema. Reach for an [Agent node](/workflow-builder/nodes/agent) only when the model must decide which tools to call and loop until it is done; an agent costs more credits because it records usage on every iteration.
  </Accordion>

  <Accordion title="I need to call an external service" icon="plug">
    Use a [Tool node](/workflow-builder/nodes/tool) to call one [integration](/integrations/overview) tool with values you control (it honors both `parameter_defaults` and `parameter_overrides`). Use a [Function node](/workflow-builder/nodes/function) for a raw HTTP request, an outbound webhook, or schema validation without an integration. Let an [Agent node](/workflow-builder/nodes/agent) call tools only when the model should choose them.
  </Accordion>

  <Accordion title="I need company knowledge or RAG context" icon="book-open">
    Use a [Knowledge node](/workflow-builder/nodes/knowledge) to retrieve chunks from a [knowledge base](/concepts/knowledge-rag), then feed `{{node_id.context}}` into a downstream [LLM node](/workflow-builder/nodes/llm) prompt.
  </Accordion>

  <Accordion title="I need to reshape or combine data" icon="shuffle">
    Use a [Transformer node](/workflow-builder/nodes/transformer) for string, object, array, type, date, and math operations — picking fields, mapping a list, parsing JSON, formatting a date, and so on — without writing code.
  </Accordion>

  <Accordion title="I need to branch or loop" icon="split">
    Use a [Conditional node](/workflow-builder/nodes/conditional): an expression branch for deterministic routing, an LLM decision for fuzzy routing, or a loop (`for` / `foreach` / `while`) to iterate over data.
  </Accordion>

  <Accordion title="I need to validate or sanitize content" icon="shield-check">
    Use a [Guardrails node](/workflow-builder/nodes/guardrails) to check content against a JSON schema or regex, detect and mask PII, then block, warn, transform, or route on failure.
  </Accordion>

  <Accordion title="I need a human to approve or supply input" icon="hand">
    Use an [Interrupt node](/workflow-builder/nodes/interrupt). It pauses the run and waits for a structured answer that you resume with — see [Human-in-the-loop resume](/realtime/hitl).
  </Accordion>
</AccordionGroup>

## Worked example: a three-node retrieve-and-summarize flow

This example chains a [Knowledge node](/workflow-builder/nodes/knowledge), an [LLM node](/workflow-builder/nodes/llm), and a [Guardrails node](/workflow-builder/nodes/guardrails). It shows the output-to-state convention and `{{node_id.field}}` references end to end. Each node writes under its own id; the next node reads it by reference.

<Steps>
  <Step title="Retrieve context from a knowledge base">
    `node_kb` queries the managed store and writes `{total_results, chunks, context}` under `node_kb`. The query itself is a reference to the run input.
  </Step>

  <Step title="Summarize with an LLM">
    `node_summary` reads `{{node_kb.context}}` into its user prompt and writes the model's answer under `node_summary`.
  </Step>

  <Step title="Validate the answer">
    `node_guard` checks `{{node_summary}}` for PII and writes its validation envelope under `node_guard`, blocking the run if a check fails.
  </Step>
</Steps>

```json title="Workflow nodes (excerpt)" theme={null}
{
  "nodes": [
    {
      "id": "node_kb",
      "type": "knowledge",
      "name": "Search handbook",
      "knowledge_config": {
        "credential_id": "cred_kb_handbook",
        "provider_type": "modulexdb",
        "query": "{{input}}",
        "top_k": 5,
        "min_score": 0.3,
        "max_tokens": 2000,
        "output_format": "context"
      }
    },
    {
      "id": "node_summary",
      "type": "llm",
      "name": "Summarize",
      "llm_config": {
        "llm": {
          "integration_name": "modulexai",
          "provider_id": "anthropic",
          "model_id": "claude-haiku-3.5",
          "temperature": 0.4
        },
        "system_prompt": "You answer strictly from the provided context.",
        "user_prompt": "Question: {{input}}\n\nContext:\n{{node_kb.context}}"
      }
    },
    {
      "id": "node_guard",
      "type": "guardrails",
      "name": "Check PII",
      "guardrails_config": {
        "source": "{{node_summary}}",
        "pii_detection": { "enabled": true, "action": "block" },
        "on_failure": "block"
      }
    }
  ],
  "edges": [
    { "source": "__start__", "target": "node_kb" },
    { "source": "node_kb", "target": "node_summary" },
    { "source": "node_summary", "target": "node_guard" }
  ]
}
```

After a successful run, the final state holds one key per node id:

<ResponseField name="node_kb" type="object">
  The Knowledge node's result.

  <Expandable title="properties">
    <ResponseField name="node_kb.total_results" type="integer">Number of chunks retrieved.</ResponseField>
    <ResponseField name="node_kb.chunks" type="array">The retrieved chunks (present when `output_format` is `chunks` or `both`).</ResponseField>
    <ResponseField name="node_kb.context" type="string">The chunks formatted into a single context string (referenced by `node_summary`).</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="node_summary" type="string">
  The LLM node's answer — written directly under the node id as a string.
</ResponseField>

<ResponseField name="node_guard" type="object">
  The Guardrails node's validation envelope.

  <Expandable title="properties">
    <ResponseField name="node_guard.valid" type="boolean">Whether every enabled check passed.</ResponseField>
    <ResponseField name="node_guard.validations" type="object">Per-check results (`json_validation`, `regex_validation`, `hallucination_check`, `pii_detection`).</ResponseField>
    <ResponseField name="node_guard.original_data" type="any">The input that was checked.</ResponseField>
    <ResponseField name="node_guard.transformed_data" type="any">The output after any masking/transform.</ResponseField>
    <ResponseField name="node_guard.blocked" type="boolean">Present and `true` when `on_failure` is `block` and a check failed.</ResponseField>
  </Expandable>
</ResponseField>

### Run it

Run the workflow over the API or an SDK. Streaming the run lets you watch each `node_update` arrive — one per node, keyed by `node` with the per-node `output` — followed by `done`. See [Run via API](/workflow-builder/execution/api-endpoint) and [SSE run streaming](/realtime/sse-streaming) for the full event taxonomy.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.modulex.dev/workflows/run" \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: org_xxx" \
    -H "Content-Type: application/json" \
    -d '{
      "workflow_id": "wf_xxx",
      "input": "What is our refund policy?"
    }'
  ```

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

  client = ModuleX(
      api_key="mx_live_xxx",
      organization_id="org_xxx",
  )

  run = client.workflows.run(
      workflow_id="wf_xxx",
      input="What is our refund policy?",
  )
  print(run)
  ```

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

  const client = new ModuleX({
    apiKey: "mx_live_xxx",
    organizationId: "org_xxx",
  });

  const run = await client.workflows.run("wf_xxx", {
    input: "What is our refund policy?",
  });
  console.log(run);
  ```
</CodeGroup>

<Warning>
  The run surface is behind the [billing gate](/billing/usage-gating). When an organization is out of credits or over a limit, the run endpoint returns a flat denial envelope — `{code, layer, key, current, limit, reason}` — as **402 / 403 / 429**, not the plain `{detail}` shape used by CRUD routes. See [Errors & status codes](/api-reference/errors) for all three envelope shapes.
</Warning>

## Per-node references

Each node's page documents every config field with its type, default, and required/optional flag, every input it reads, the exact shape it writes, every error it can raise, and its credit impact.

<CardGroup cols={3}>
  <Card title="LLM node" icon="message-bot" href="/workflow-builder/nodes/llm">
    Call a model with prompts and optional structured output.
  </Card>

  <Card title="Agent node" icon="robot" href="/workflow-builder/nodes/agent">
    A model-plus-tools loop that runs until done.
  </Card>

  <Card title="Tool node" icon="plug" href="/workflow-builder/nodes/tool">
    Call one integration tool deterministically.
  </Card>

  <Card title="Knowledge node" icon="book-open" href="/workflow-builder/nodes/knowledge">
    Retrieve RAG context from a knowledge base.
  </Card>

  <Card title="Function node" icon="code" href="/workflow-builder/nodes/function">
    HTTP request, webhook, or schema validation.
  </Card>

  <Card title="Transformer node" icon="shuffle" href="/workflow-builder/nodes/transformer">
    Reshape, map, filter, and convert data.
  </Card>

  <Card title="Conditional node" icon="split" href="/workflow-builder/nodes/conditional">
    Branch on expressions or LLM decisions, or loop.
  </Card>

  <Card title="Guardrails node" icon="shield-check" href="/workflow-builder/nodes/guardrails">
    Validate content and block, warn, transform, or route.
  </Card>

  <Card title="Interrupt node" icon="hand" href="/workflow-builder/nodes/interrupt">
    Pause for a human answer, then resume.
  </Card>
</CardGroup>

## Related

<CardGroup cols={2}>
  <Card title="Workflow engine & nodes" icon="diagram-project" href="/concepts/workflow-engine">
    How nodes compile into a graph, the run state, edges, and loops.
  </Card>

  <Card title="Variables & references" icon="brackets-curly" href="/workflow-builder/variables-and-references">
    The full `{{node_id.field}}` resolution rules, reducers, and run state.
  </Card>

  <Card title="Error handling & retries" icon="triangle-exclamation" href="/workflow-builder/error-handling-retries">
    How node failures surface and how retries behave.
  </Card>

  <Card title="Glossary" icon="book" href="/reference/glossary">
    Canonical ModuleX terminology, including every node term.
  </Card>
</CardGroup>
