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

# Tool node

> Call one integration action deterministically inside a workflow. Full ToolNodeConfig and ToolDefinition reference: input_mapping, parameter_defaults, parameter_overrides, credential resolution, per-action credit cost, custom MCP tools, inputs/outputs, and errors.

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 `tool` node calls a single integration action — one tool such as `github.create_issue` or `tavily.web_search` — with parameters you control, then writes the action's result into run state under the node's own `id`. Use it when you want a deterministic, single-purpose step: you pick the integration, the action, and exactly what goes in. For a step where a language model decides which tools to call and loops over them, use the [agent node](/workflow-builder/nodes/agent) instead.

ModuleX exposes 600+ tools across 175 integrations. Browse them in the [integrations overview](/integrations/overview) and the [catalog](/integrations/catalog). To call a tool you have not configured yet, first connect the service — see [connect an integration](/guides/connect-an-integration) and [managing credentials](/integrations/managing-credentials).

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

## What the node does

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

1. Resolves `input_mapping` against the current run state, replacing every `{{...}}` token with its resolved value. Keys whose resolved value is `null` or an empty string are dropped from the call.
2. Loads the tool for `tool.integration_name` + `tool.service_name`, scoped to your organization, and resolves a credential (see [credential resolution](#credential-resolution)).
3. Invokes the action. The credential and auth data are injected for you — they never appear in your `input_mapping`.
4. Records per-action usage for billing (managed credentials only — see [credit impact](#credit-impact)).
5. Unwraps the action's result and writes it into run state under the node's `id` (see [inputs & outputs](#inputs-and-outputs)).

Each `tool` node runs the action exactly once per run. When several tool nodes run in parallel, each invocation gets its own isolated database session so concurrent calls do not interfere.

<Note>
  The output of a tool node is always stored in run state under the node's `id` — for example a node with `id: "create_issue_1"` writes to `{{create_issue_1}}`. The legacy `output_key` field is deprecated and ignored; do not rely on it. See [inputs & outputs](#inputs-and-outputs).
</Note>

## Configuration (`ToolNodeConfig`)

These fields live on the node's `tool_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 `tool_config`).

<ParamField path="tool" type="ToolDefinition object" required>
  The action to call. Required. Selects the integration, the action, an optional credential, and optional parameter values. See [the `tool` object](#the-tool-object) below.
</ParamField>

<ParamField path="input_mapping" type="object" default="{}">
  A map of action-parameter name to value. Each value may be a literal or a `{{node_id.field}}` reference; references resolve against run state at run time. This is the primary way to feed data into the action. Keys whose resolved value is `null` or `""` are skipped. See [feeding parameters](#feeding-parameters).
</ParamField>

<Accordion title="Deprecated fields — do not use in new workflows">
  <ParamField path="output_key" type="string" deprecated>
    Deprecated. Output is always stored under the node `id`. This field is ignored.
  </ParamField>
</Accordion>

### The `tool` object

The `tool` field is a `ToolDefinition`. It identifies which integration action to call and, optionally, which credential and which baseline/forced parameter values to apply.

<ParamField path="tool.integration_name" type="string" required>
  The integration on the wire, for example `github`, `tavily`, or `slack`. Required. Use the literal `mcp_server` to call a tool from a [custom MCP server](#custom-mcp-tools). Browse names in the [catalog](/integrations/catalog).
</ParamField>

<ParamField path="tool.service_name" type="string" required>
  The action (service) name within the integration, for example `create_issue`, `web_search`, or `send_message`. Required. In ModuleX the action name and the underlying tool function name are the same string. For an MCP server, this is the MCP tool name. See [available actions](#available-actions).
</ParamField>

<ParamField path="tool.credential_id" type="string">
  A specific stored credential to use for this call. Optional. If omitted, ModuleX resolves a credential for the integration in the current organization (see [credential resolution](#credential-resolution)). Required when `integration_name` is `mcp_server`, where it points at the MCP server credential.
</ParamField>

<ParamField path="tool.parameter_defaults" type="object">
  Baseline parameter values, applied as a base layer beneath caller-supplied values. Optional. Supports `{{node_id.field}}` references. On the tool node these are intended as deterministic defaults that fill parameters you do not provide elsewhere. (On the [agent node](/workflow-builder/nodes/agent), only `parameter_defaults` apply — the model's chosen arguments win, and defaults fill what the model omitted.)
</ParamField>

<ParamField path="tool.parameter_overrides" type="object">
  Forced parameter values, intended to be applied regardless of other values. Optional. Supports `{{node_id.field}}` references. The tool node is the node type that exposes overrides; the agent node ignores them. Use overrides to pin a parameter that must not vary — for example forcing a fixed `repo` or `channel`.
</ParamField>

<Warning>
  The tool node is the only node type whose `ToolDefinition` is meant to use **both** `parameter_defaults` and `parameter_overrides` (the agent node uses defaults only). In current source, however, the tool-node execution path applies only `input_mapping` to the action and does not visibly merge `parameter_defaults`/`parameter_overrides` before invoking. Until that is confirmed, **put the values you depend on in `input_mapping`** rather than relying on the parameter fields alone.
</Warning>

### Retry configuration

The `tool` node is retry-wrapped. You can attach a `retry_config` to the node definition itself (not inside `tool_config`) to control how a failed action is retried. If you omit it, the engine applies its default retry policy. 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.
</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>

See [error handling & retries](/workflow-builder/error-handling-retries) for how retries surface in the run stream.

## Feeding parameters

You provide the action's parameters through `input_mapping`. Each entry maps an action-parameter name to a literal value or a `{{...}}` reference:

```json input_mapping theme={null}
{
  "owner": "octocat",
  "repo": "hello-world",
  "title": "{{triage_1.summary}}",
  "body": "Reported by {{intake_1.reporter}} on {{intake_1.date}}",
  "labels": ["bug", "triage"]
}
```

Resolution rules (the same model used everywhere — see [variables & references](/workflow-builder/variables-and-references)):

* A value that is exactly `{{node_id.field}}` keeps its native type (string, number, object, or array).
* A reference embedded in surrounding text is string-substituted; objects and arrays are JSON-encoded into the string.
* `{{node_id}}` resolves to an upstream node's whole output; `{{node_id.field}}` resolves a nested value by dot/bracket path, for example `{{search_1.results[0].url}}`.
* An out-of-range index or missing key resolves to nothing; the reference is left intact in any string it appears in.
* Keys whose resolved value is `null` or `""` are dropped from the call (so an unresolved optional parameter does not send an empty value).

You never put credentials or auth fields (such as `api_key`, `token`, `access_token`) in `input_mapping`. ModuleX strips those from the action's caller-facing schema and injects the resolved credential internally before calling the action. See [credential resolution](#credential-resolution).

### Available actions

Each integration exposes a fixed set of actions, each with its own parameters and output shape. Find them on the integration's catalog page in the [catalog](/integrations/catalog), or programmatically with the [list integrations](/api-reference/overview) endpoint. An action's parameters are declared by the integration's manifest contract; see [manifest & schema contract](/integrations/building/manifest-schema) and [the @tool function contract](/integrations/building/tool-contract).

## Credential resolution

A tool node needs a credential for its integration. You can pin one with `tool.credential_id`, or let ModuleX resolve one for the organization. When `credential_id` is omitted, resolution is:

<Steps>
  <Step title="Explicit credential">
    If `tool.credential_id` is set, that credential is used (it must belong to the organization and match the integration).
  </Step>

  <Step title="Default credential">
    Otherwise the integration's default credential for the organization (the one marked default) is used.
  </Step>

  <Step title="Any valid credential">
    Otherwise any valid credential for the integration is used — preferring your own connected credentials over the ModuleX-managed key, newest first.
  </Step>
</Steps>

What ModuleX does with the resolved credential, by auth type:

* It decrypts the stored auth data, scoped to your organization and that credential.
* For OAuth2 credentials, it refreshes the access token automatically if it expires within five minutes, then persists the refreshed token.
* It injects the right field for the auth type (`access_token` for OAuth2, `token` for bearer auth, `api_key` for API-key and managed keys), plus any other stored fields the action declares (for example a `project_id` or `base_url` entered once when you created the credential).

For how credentials are created, scoped, and encrypted, see [authentication & credentials](/integrations/authentication), [credentials & OAuth2](/concepts/credentials-oauth), and [data security & encryption](/security/data-encryption).

<Note>
  Token reconnection during a run resolves through the stored credential. The app's manual OAuth2 refresh button is a [known limitation](/reference/known-limitations) — when a credential cannot be refreshed, reconnect the integration rather than relying on that button.
</Note>

## Custom MCP tools

A tool node can call a tool from an external [Model Context Protocol (MCP)](/integrations/building/custom-mcp) server you have connected, instead of a built-in integration. Set the node's `tool.integration_name` to the literal `mcp_server` and point `tool.credential_id` at your MCP server credential:

```json MCP tool definition theme={null}
{
  "tool": {
    "integration_name": "mcp_server",
    "service_name": "search_documents",
    "credential_id": "cred_mcp_9f2a"
  },
  "input_mapping": {
    "query": "{{intake_1.question}}",
    "limit": 5
  }
}
```

How it differs from a catalog integration:

* `tool.credential_id` is **required** — the MCP server's URL, headers, and transport are stored on that credential, not in the catalog.
* `tool.service_name` is the MCP tool's name as advertised by your server; ModuleX connects to the server, lists its tools, and selects the one that matches.
* Responses are parsed back into structured data so an MCP tool's output reads like a built-in tool's output.

Connect an MCP server before referencing it here — see [custom MCP servers](/integrations/building/custom-mcp) for the credential fields (server URL, headers, transport) and setup.

## Inputs and outputs

### Inputs

The tool node has no fixed input fields of its own. Its inputs are the action's parameters, supplied through `input_mapping` (and the optional `tool.parameter_defaults` / `tool.parameter_overrides`). Reference upstream values with `{{node_id}}` and `{{node_id.field}}`. See [feeding parameters](#feeding-parameters) and [variables & references](/workflow-builder/variables-and-references).

### Outputs

The node writes one value into run state under its `id`. ModuleX **unwraps** the action's envelope: actions return a wrapper shaped like `{success, action, result: {...}}`, and the node flattens the inner `result` fields up to the top level so you can reference them directly without a `.result.` segment.

<ResponseField name="{node_id}" type="object">
  The unwrapped action output.

  <Expandable title="What unwrapping does">
    <ResponseField name="success" type="boolean">
      The action's own success flag (preserved at the top level).
    </ResponseField>

    <ResponseField name="action" type="string">
      The action name (preserved at the top level when present).
    </ResponseField>

    <ResponseField name="<action fields>" type="object | array | string">
      The fields from the action's `result` object, merged to the top level. For example, a `github.create_issue` call surfaces its `issue` object directly, so you can reference `{{create_issue_1.issue.number}}`.
    </ResponseField>

    <ResponseField name="result" type="object">
      The original nested `result` object is also kept for backward compatibility, so existing `{{node_id.result.field}}` references continue to work.
    </ResponseField>
  </Expandable>
</ResponseField>

Concretely, a `github.create_issue` action returns:

```json action output (before unwrapping) theme={null}
{
  "success": true,
  "issue": {
    "number": 42,
    "title": "Bug",
    "state": "open",
    "url": "https://github.com/octocat/hello-world/issues/42"
  }
}
```

A downstream node can read `{{create_issue_1.issue.number}}` or `{{create_issue_1.success}}` directly.

## Streaming

A tool node streams at the **node level** over [SSE](/realtime/sse-streaming). When the action finishes, the engine publishes a `node_update` event carrying the node's unwrapped 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. See [SSE run streaming](/realtime/sse-streaming).
</Note>

## Credit impact

There is **no fixed per-node credit charge** decided by the node type. A tool action is metered per call, and whether it costs credits depends on the credential:

* **ModuleX-managed key** — the call is charged a flat per-action cost in [credits](/billing/credits). The base is **1 credit** per action (anchored to $0.01 at 100 credits = $1.00), multiplied by a per-integration multiplier (default `1.0`). So a typical managed action costs 1 credit; some integrations carry a higher multiplier. Managed-key calls are also subject to the monthly credit limit — when the plan budget is exhausted and wallet overage is unavailable, the call is denied.
* **Your own connected credential (BYOK)** — usage is recorded for analytics, but **no credits are charged**; you pay the upstream provider directly with no ModuleX markup.

Usage is logged for both successful and failed managed calls. Logging is best-effort: if writing the usage record fails, the tool call is not failed because of it.

<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 & limits](/billing/usage-gating).
</Note>

The workflow run goes through the [usage gate](/billing/usage-gating), which admits the run before it starts. Once a run is executing the gate is best-effort: if a managed tool call cannot be billed because credits are exhausted, the node's error event carries a stable `reason` token (`credit_exhausted`) so clients can detect a budget stop deterministically — see [errors](#errors).

## Errors

The tool node surfaces failures through the run's node-error event after retries are exhausted. The underlying tool runtime raises (it does not return an error envelope), and the engine lets these fail the node:

<Accordion title="Missing tool_config">
  A `tool` node with no `tool_config` fails compilation with `Tool node <id> missing tool_config`. Ensure the `tool` object with `integration_name` and `service_name` is set.
</Accordion>

<Accordion title="Tool could not be loaded">
  If the integration or action cannot be loaded — an unknown `integration_name`/`service_name`, or an MCP server that returns no matching tool — the node fails with `Failed to load tool: <integration>.<action>`. Check the names against the [catalog](/integrations/catalog) and, for MCP, that the credential and tool name are correct.
</Accordion>

<Accordion title="No credential found">
  If no credential exists for the integration in your organization, the runtime raises a no-credential error and the node fails. Connect the integration first — see [authentication & credentials](/integrations/authentication) and [managing credentials](/integrations/managing-credentials).
</Accordion>

<Accordion title="Credit exhaustion (managed key)">
  If a managed-key call cannot be billed because the monthly credit budget is exhausted and no wallet overage is available, the runtime raises a credit-limit error whose stable code is `credit_exhausted`; the node's `node_error` event carries it as `reason`. Resolve by topping up the [wallet](/billing/wallet) or upgrading your [plan](/billing/plans). BYOK credentials are not credit-limited.
</Accordion>

<Accordion title="Credential resolution or OAuth refresh failure">
  A decryption or OAuth2-refresh failure surfaces as a tool execution error and fails the node. Reconnect the affected integration. The app's manual OAuth refresh control is a [known limitation](/reference/known-limitations).
</Accordion>

<Accordion title="Action raised an error">
  Any error the action itself raises (a 4xx/5xx from the upstream service, a validation failure, an unexpected exception) surfaces as a tool execution error. Provider/network errors are retried per `retry_config`; 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>

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

## Full example

A two-node workflow: an upstream `llm` node triages an incoming message into a structured object, and a `tool` node opens a GitHub issue from it. The first tab is the tool node definition; the remaining tabs run a deployed workflow that contains it.

Running a workflow is asynchronous: the run call returns immediately with run metadata (`status` is `running`), and you observe node output by streaming events. The created issue arrives on the `node_update` event for `create_issue_1`. See [SSE run streaming](/realtime/sse-streaming) and [run a workflow](/guides/run-a-workflow).

<CodeGroup>
  ```json Tool node definition theme={null}
  {
    "id": "create_issue_1",
    "type": "tool",
    "name": "Open GitHub issue",
    "x": 720,
    "y": 160,
    "retry_config": {
      "max_attempts": 3,
      "initial_interval": 1.0,
      "backoff_factor": 2.0,
      "retry_on_error_types": ["TimeoutError", "ConnectionError", "HTTPError"]
    },
    "tool_config": {
      "tool": {
        "integration_name": "github",
        "service_name": "create_issue",
        "credential_id": "cred_gh_7a1c",
        "parameter_overrides": {
          "owner": "octocat",
          "repo": "hello-world"
        }
      },
      "input_mapping": {
        "title": "{{triage_1.summary}}",
        "body": "Priority: {{triage_1.priority}}\n\nReported by {{intake_1.reporter}}.",
        "labels": ["triage", "{{triage_1.priority}}"]
      }
    }
  }
  ```

  ```bash cURL theme={null}
  # 1. Start the run (POST /workflows/run). Returns run metadata immediately.
  curl 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": { "reporter": "alex", "message": "Invoice double-charged me." }
          }
        }'
  # -> { "status": "running", "run_id": "run_8b21", "thread_id": "thr_44", ... }

  # 2. Stream events (SSE). Each frame is `data: <json>`; switch on the `type` field.
  #    The created issue arrives on the create_issue_1 node_update event.
  curl -N https://api.modulex.dev/workflows/listen/run_8b21 \
    -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \
    -H "X-Organization-ID: org_4d2b18"
  ```

  ```python Python theme={null}
  import asyncio
  from modulex import Modulex


  async def main():
      async with Modulex(
          api_key="mx_live_xxxxxxxxxxxxxxxxxxxx",
          organization_id="org_4d2b18",
      ) as client:
          # 1. Start the run (returns immediately with run metadata).
          run = await client.executions.run(
              workflow_id="wf_7f3a9c",
              input={
                  "intake_1": {"reporter": "alex", "message": "Invoice double-charged me."},
              },
          )

          # 2. Stream events; the created issue arrives on create_issue_1's node_update.
          async for event in client.executions.listen(run.run_id):
              if event.event == "node_update" and event.data.get("node") == "create_issue_1":
                  output = event.data["output"]["create_issue_1"]
                  print(output["issue"]["number"])  # -> 42
                  print(output["issue"]["url"])


  asyncio.run(main())
  ```

  ```javascript JavaScript theme={null}
  import { Modulex } from "modulex-js";

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

  // 1. Start the run (returns immediately with run metadata).
  const run = await client.executions.run({
    workflowId: "wf_7f3a9c",
    input: {
      intake_1: { reporter: "alex", message: "Invoice double-charged me." },
    },
  });

  // 2. Stream events; the created issue arrives on create_issue_1's node_update.
  for await (const event of client.executions.listen(run.run_id)) {
    if (event.type === "node_update" && event.node === "create_issue_1") {
      const output = event.output.create_issue_1;
      console.log(output.issue.number); // -> 42
      console.log(output.issue.url);
    }
  }
  ```
</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).

## Related

<CardGroup cols={2}>
  <Card title="Agent node" icon="robot" href="/workflow-builder/nodes/agent">
    When a model should decide which tools to call and loop, not a fixed single action.
  </Card>

  <Card title="Integrations overview" icon="plug" href="/integrations/overview">
    Connect services and browse the 600+ tools across 175 integrations.
  </Card>

  <Card title="Custom MCP servers" icon="server" href="/integrations/building/custom-mcp">
    Connect an external MCP server and call its tools from a tool node.
  </Card>

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