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

# How the Assistant uses tools

> How the ModuleX Assistant discovers integrations, calls tool actions with your organization's credentials, requests missing credentials over human-in-the-loop, and what each tool costs in credits — BYOK vs managed.

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 Assistant is the workflow-independent agentic chat: it searches your connected
integrations, picks an action, and runs it with your organization's credentials — no
workflow graph required. This page is the technical reference for the **tool side** of
that loop: the tools the Assistant has, the parameters and outputs of each, the approval
behavior, how it asks for a missing credential over human-in-the-loop, and the credit
impact of every call.

For the surrounding agentic loop (reason → act → observe → finish), see
[How the Assistant works](/assistant/how-it-works). For pause-and-resume mechanics shared
by every interrupt the Assistant raises, see
[Human-in-the-loop](/assistant/human-in-the-loop). To connect the integrations the
Assistant calls, start at [Integrations overview](/integrations/overview).

<Note>
  The Assistant runs the `assistant` profile of the shared agent core. It has **no workflow
  tools at all** — it cannot list, build, run, or edit a [workflow](/concepts/workflows-and-runs).
  Every tool below is scoped to discovering integrations, calling integration actions,
  retrieving [knowledge](/concepts/knowledge-rag), and asking you questions.
</Note>

## The Assistant's toolset

The Assistant is handed a fixed set of tools at run start. There are three discovery
tools, one execution tool, one knowledge tool, and the human-in-the-loop tools (one of
which is `request_credential`).

| Tool                                                                                   | Purpose                                                                       |        Pauses the run?       |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | :--------------------------: |
| `get_available_integrations`                                                           | Browse the compact catalog of tools, LLM providers, and knowledge providers.  |              No              |
| `get_integration_details`                                                              | Fetch the full action descriptions and parameter schemas for one integration. |              No              |
| `get_organization_credentials`                                                         | List which credentials the organization already has (no secrets).             |              No              |
| `execute_integration_tool`                                                             | Run **one** integration action with the organization's credentials.           | Only for destructive actions |
| `search_knowledge`                                                                     | Retrieve from a connected [knowledge base](/concepts/knowledge-rag).          |              No              |
| `request_credential`                                                                   | Ask you to connect an integration that has no credential yet.                 |          Yes (HITL)          |
| `ask_user_choice` / `ask_user_yes_no` / `ask_user_free_text` / `ask_user_multi_choice` | Ask you a structured question.                                                |          Yes (HITL)          |

The discovery tools and execution tool together form a deliberate two-step pattern: the
Assistant first narrows the catalog, then pulls the exact parameter schema for the action
it chose, then executes. The sections below document each in turn.

<MediaEmbed id="MX-MEDIA-3240" type="image" caption={"A flow diagram of the Assistant's discover → detail → execute tool sequence with the credential branch."} />

## Tool discovery

### Browse the catalog — `get_available_integrations`

The first discovery step returns a **compact** index of everything the organization could
connect, so the full portfolio fits in the agent's context without bloating it. For tools
you get the integration name, a short description, categories, and **action names only** —
no per-action descriptions or parameter schemas at this step.

<ParamField path="integration_type" type="string" default="all">
  One of `tools`, `llm_providers`, `knowledge_providers`, or `all`. Filters which catalog
  segment is returned.
</ParamField>

<ParamField path="query" type="string">
  Optional case-insensitive substring filter. It matches name, display name, description,
  category, and action name. A narrow query (for example `query="slack"`) keeps the
  response small **without** risking that a relevant integration is dropped — filtering is
  exact-substring, not lossy truncation.
</ParamField>

The response groups results by segment. Tool entries carry action names only; you must
call `get_integration_details` for the parameter schema before you can configure a call.

<ResponseField name="tools" type="object[]">
  <Expandable title="tool entry">
    <ResponseField name="name" type="string">Catalog integration id, for example `github`.</ResponseField>
    <ResponseField name="display_name" type="string">Human-readable name, for example `GitHub`.</ResponseField>
    <ResponseField name="description" type="string">Short description (truncated to 100 characters).</ResponseField>
    <ResponseField name="categories" type="string[]">Catalog categories.</ResponseField>
    <ResponseField name="actions" type="string[]">Action **names** only — descriptions and parameters come from `get_integration_details`.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="llm_providers" type="object[]">
  Each entry adds a `models` list of `{id, display_name, max_output_tokens}`. See
  [LLM providers](/integrations/llm-providers/overview).
</ResponseField>

<ResponseField name="knowledge_providers" type="object[]">
  Each entry carries `name`, `display_name`, `description`, and `categories`. See
  [Knowledge providers](/integrations/knowledge-providers/overview).
</ResponseField>

### Read an integration's actions — `get_integration_details`

The second discovery step. Call it after choosing an integration to get the parameter
schemas you need to actually configure an action.

<ParamField path="integration_name" type="string" required>
  The integration name, for example `tavily`, `firecrawl`, or `github`.
</ParamField>

<ParamField path="action_name" type="string">
  Optional. Restrict the response to a single action to keep it small. An unknown action
  returns `{error: "Action '<name>' not found in <integration>"}`.
</ParamField>

For a tool integration, each action returns its `name`, a `description` (truncated to 300
characters), and a `parameters` array. Each parameter carries `name`, `type`, `required`,
`description`, and — when defined in the manifest — `enum` and `default`:

```json Example: get_integration_details("github", "create_issue") theme={null}
{
  "name": "github",
  "integration_type": "tool",
  "actions": [
    {
      "name": "create_issue",
      "description": "Create a new issue in a repository.",
      "parameters": [
        { "name": "owner", "type": "string", "required": true, "description": "Repository owner" },
        { "name": "repo", "type": "string", "required": true, "description": "Repository name" },
        { "name": "title", "type": "string", "required": true, "description": "Issue title" },
        { "name": "body", "type": "string", "required": false, "description": "Issue body (markdown)" },
        { "name": "labels", "type": "array", "required": false, "description": "Label names" }
      ]
    }
  ]
}
```

An unknown integration returns `{error: "Integration '<name>' not found"}`. The parameter
`type` is one of `string`, `integer`, `number`, `boolean`, `array`, or `object` — the same
types the [manifest schema](/integrations/building/manifest-schema) defines.

### See existing credentials — `get_organization_credentials`

The Assistant uses this to decide whether it already has a way to authenticate before it
calls an action. It returns only **safe** fields — never a token, key, or secret.

<ParamField path="integration_name" type="string">
  Optional. Return only credentials for this integration (for example `slack`). Pass it
  when the Assistant already knows which integration it needs so the response stays small.
</ParamField>

<ResponseField name="(array)" type="object[]">
  <Expandable title="credential entry">
    <ResponseField name="credential_id" type="string">The credential's id (UUID).</ResponseField>
    <ResponseField name="integration_name" type="string">The integration this credential is for.</ResponseField>
    <ResponseField name="display_name" type="string">The credential's display name.</ResponseField>
    <ResponseField name="auth_type" type="string">One of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`. See [Authentication & credentials](/integrations/authentication).</ResponseField>
    <ResponseField name="is_default" type="boolean">Whether this is the default credential for its integration.</ResponseField>
    <ResponseField name="is_valid" type="boolean">Whether the credential is currently valid.</ResponseField>
  </Expandable>
</ResponseField>

## Calling a tool

### `execute_integration_tool`

This is the only tool that performs a real action. The Assistant calls it with the
integration id and action name it discovered, plus the action parameters. It runs exactly
**one** action per call.

<ParamField path="integration_name" type="string" required>
  Catalog integration id, for example `github`.
</ParamField>

<ParamField path="tool_name" type="string" required>
  The action/service name, for example `list_repositories`.
</ParamField>

<ParamField path="parameters" type="object" default="{}">
  The action parameters, matching the schema from `get_integration_details`. **Never**
  include credentials or tokens — the runtime injects them automatically from the
  organization's credential. Credential fields such as `api_key`, `token`,
  `access_token`, `bearer_token`, `auth_type`, and `auth_data` are stripped from the
  schema the Assistant sees, so it cannot supply them even if it tries.
</ParamField>

Internally, the call resolves a credential, decrypts it, refreshes an OAuth token if it
is within five minutes of expiry, maps the parameters, invokes the underlying action, and
returns the raw action output. Credential selection follows a fixed precedence: an
explicit credential, then the integration's default credential, then the most recent
valid credential — preferring **your own** credentials over managed ones. See
[Credentials & OAuth2](/concepts/credentials-oauth) for the full resolution rules.

#### Return shape

`execute_integration_tool` always returns a dict with a `status` field. It does **not**
raise to the agent — every failure is converted to a structured status the Assistant can
react to mid-stream.

<ResponseField name="status" type="string" required>
  One of `ok`, `cancelled`, `needs_credential`, `credit_limit`, or `error`.
</ResponseField>

<ResponseField name="integration_name" type="string">Echoes the requested integration.</ResponseField>
<ResponseField name="tool_name" type="string">Echoes the requested action.</ResponseField>

<ResponseField name="result" type="object">
  Present when `status` is `ok`. The raw action output (a dict, list, or string),
  truncated to 8000 characters before it re-enters the agent loop. The credential and
  execution envelope around the action are stripped — the Assistant sees only the bare
  action result.
</ResponseField>

<ResponseField name="message" type="string">
  Present for `needs_credential` and `credit_limit` — a human-readable explanation the
  Assistant can act on (for example, "call request\_credential, then retry").
</ResponseField>

<ResponseField name="reason" type="string">
  Present for `cancelled` (the user declined an approval) and `credit_limit` (the stable
  token `credit_exhausted`).
</ResponseField>

<ResponseField name="error" type="string">
  Present when `status` is `error` — the failure text, truncated to 500 characters.
</ResponseField>

```json status: ok theme={null}
{
  "status": "ok",
  "integration_name": "github",
  "tool_name": "list_repositories",
  "result": { "repositories": [{ "name": "hello-world", "private": false }] }
}
```

<ResponseField name="status values" type="enum">
  <Expandable title="what each status means">
    <ResponseField name="ok">The action ran and returned a result.</ResponseField>
    <ResponseField name="cancelled">A destructive action was gated for approval and you declined.</ResponseField>
    <ResponseField name="needs_credential">No credential exists for the integration in this organization. The Assistant should call `request_credential`, then retry.</ResponseField>
    <ResponseField name="credit_limit">A managed (`modulex_key`) credential hit the plan's monthly credit ceiling with no wallet overage available. `reason` is `credit_exhausted`.</ResponseField>
    <ResponseField name="error">The action raised, the action name was not found, or any other unexpected failure.</ResponseField>
  </Expandable>
</ResponseField>

### The approval gate — reads run, destructive actions pause

The Assistant applies a **safety** gate (not an authorization gate — your org membership
already authorizes the call) before it executes. The policy differs from the
[AI Composer](/concepts/ai-composer) in the builder, and the difference matters:

<Warning>
  On the **Assistant surface**, only **catastrophic** actions pause for approval. Actions
  whose names contain `delete_`, `_delete`, `drop_`, `truncate`, `purge`, `destroy`,
  `wipe`, or raw query execution (`execute_query`, `execute_statement`, `execute_raw_query`,
  `raw_query`) always require your explicit yes/no. **Everything else — including ordinary
  creates and sends, like posting a Slack message or creating a Linear issue — runs
  immediately.** The Assistant is a "just do what I asked" surface; asking before every
  write is impractical. The Composer in the builder is stricter: it default-denies every
  write or unknown action against a frozen read-action allowlist.
</Warning>

When a destructive action is gated, the Assistant raises a yes/no
[human-in-the-loop](/assistant/human-in-the-loop) interrupt with a redacted preview of the
parameters (secret-looking keys are masked). If you answer no, the call returns
`status: cancelled` and the action never runs.

## Credential requests over human-in-the-loop

When `execute_integration_tool` returns `needs_credential`, the Assistant calls
`request_credential` to ask you to connect the integration. This raises a credential
[interrupt](/assistant/human-in-the-loop): the run pauses, the app renders a connect
popup, and the Assistant resumes once you finish.

### `request_credential`

<ParamField path="integration_name" type="string" required>
  Canonical integration slug, for example `slack` or `google_drive`. Used to resolve
  OAuth and credential storage.
</ParamField>

<ParamField path="integration_display_name" type="string" required>
  Human-readable name shown in the popup header, for example `Slack`.
</ParamField>

<ParamField path="auth_options" type="object[]" required>
  One entry per auth type you can choose. At least one is required. Each matches the
  `CredentialAuthOption` schema below.
</ParamField>

<ParamField path="integration_logo" type="string">
  Optional URL to the integration's logo for the popup.
</ParamField>

<ParamField path="pending_node_name" type="string">
  Optional. Reserved for the workflow context and not used by the Assistant (Assistant
  chats are never workflow-bound).
</ParamField>

<ParamField path="required" type="boolean" default="false">
  When `false` (the default for the Assistant), the popup shows an inline "Skip for now"
  button so you can defer connecting. Set `true` only when the task truly cannot proceed
  without the credential.
</ParamField>

<ParamField path="context" type="object">
  Optional rendering hints for the app.
</ParamField>

Each entry in `auth_options` is a `CredentialAuthOption`:

<ParamField path="auth_options[].auth_type" type="string" required>
  One of `oauth2`, `api_key`, `bearer_token`, `modulex_key`, `custom`.
</ParamField>

<ParamField path="auth_options[].display_name" type="string" required>
  The button label inside the popup.
</ParamField>

<ParamField path="auth_options[].fields" type="object[]">
  Form-field definitions for form-based auth types such as `api_key`.
</ParamField>

<ParamField path="auth_options[].oauth_initiate_endpoint" type="string">
  The full URL the app opens to start the OAuth flow. **Required when `auth_type` is
  `oauth2`** — an OAuth option without it is rejected before the interrupt fires.
</ParamField>

<ParamField path="auth_options[].setup_instructions" type="string[]">
  Optional human-readable setup steps shown beside the form or OAuth button.
</ParamField>

<ParamField path="auth_options[].test_supported" type="boolean" default="false">
  When `true`, the new credential is round-trip tested before the Assistant resumes, so an
  invalid credential surfaces as a structured failure instead of sending the Assistant
  down a broken path.
</ParamField>

### How you respond, and how the run resumes

You answer a credential request in one of three ways, and the Assistant resumes with that
result baked into the tool response:

<Steps>
  <Step title="You connect the integration">
    You complete the OAuth popup or fill the form. The credential is persisted and the run
    resumes with `kind: "credential_added"` carrying the new `credential_id`,
    `integration_name`, and `auth_type`. The Assistant retries the original action.
  </Step>

  <Step title="The connection fails">
    The run resumes with `kind: "credential_failed"` carrying an `error_code` — one of
    `oauth_denied`, `oauth_provider_error`, `invalid_credentials`, `network_error`,
    `popup_closed`, `timeout`, or `unknown` — plus a `retryable` flag. The Assistant can
    try a different auth option or report the failure.
  </Step>

  <Step title="You skip">
    When the request was not `required`, the inline "Skip for now" button resumes the run
    with `kind: "skipped"`. The Assistant proceeds without the credential.
  </Step>
</Steps>

<Note>
  **OAuth auto-resume.** When you complete an OAuth flow that the Assistant opened, the
  OAuth callback resumes the chat automatically — you do **not** make a separate resume
  call. The callback re-runs the same ownership checks and continues the run, swapping your
  live stream to the new run id. See [Human-in-the-loop](/assistant/human-in-the-loop) for
  the resume contract and stream handover.
</Note>

<Warning>
  **Reconnect, do not rely on token refresh.** The manual OAuth token-refresh path
  (`POST /credentials/{credential_id}/oauth2/refresh`, and the equivalent UI
  `refreshOAuth2` action) is currently **broken** and must not be treated as a working
  flow. ModuleX still refreshes OAuth tokens **automatically** during tool execution when a
  token is within five minutes of expiry. If a credential has fully expired or its refresh
  token is no longer valid, **reconnect the integration** — run the connect flow again to
  mint a fresh credential — rather than calling the manual refresh. This is tracked in
  [Known limitations](/reference/known-limitations).
</Warning>

## BYOK vs managed credentials

Every integration call resolves to one of two kinds of credential, and the kind decides
**who you pay and whether the call counts against your credits**.

<CardGroup cols={2}>
  <Card title="BYOK — your own credentials" icon="key">
    You connect your own provider account (OAuth, API key, or bearer token). Usage is billed
    **directly by the provider** with no ModuleX markup. BYOK tool calls are **not**
    credit-limited — they are tracked for analytics only, never gated on credits.
  </Card>

  <Card title="Managed — `modulex_key`" icon="server">
    ModuleX provides a pooled, system-managed key (`auth_type: modulex_key`) so you can call
    a tool without bringing your own account. Managed usage is **billed in credits** and is
    subject to your plan's monthly [credit](/billing/credits) ceiling. On the wire the
    managed provider is `modulexai` for tools and LLMs, and `modulexdb` for
    [managed knowledge](/platform/knowledge/managed).
  </Card>
</CardGroup>

When both exist, resolution prefers your **own** credential over the managed key. So if
you connect your own GitHub account, the Assistant uses it (no credits), and only falls
back to a managed key when you have not connected your own.

### Credit impact of a tool call

A managed tool call is metered with a per-action soft cost:

| Quantity       | Value            | Notes                                               |
| -------------- | ---------------- | --------------------------------------------------- |
| Tool base cost | **1 credit**     | Anchored to $0.01 (100 credits = $1).               |
| Effective cost | `1 × multiplier` | The integration's credit multiplier, default `1.0`. |
| BYOK tool cost | **0 credits**    | BYOK is tracked, never charged or gated.            |

The credit check runs only for managed (`modulex_key`) credentials: if the plan's monthly
budget is exhausted and no wallet overage is available, the call returns
`status: credit_limit` with `reason: credit_exhausted`. Plans with no monthly ceiling
(for example Enterprise) are never blocked here. See
[Credits & metering](/billing/credits) and [Usage gating & limits](/billing/usage-gating)
for the full model.

<Note>
  The Assistant **turn itself** is also metered separately from the tool. Each user message
  charges one run credit per turn and is admitted through the billing gate before any work
  starts. That gate, and the LLM token usage it records, are documented in
  [Permissions & limits](/assistant/permissions-and-limits) and
  [Usage gating & limits](/billing/usage-gating).
</Note>

## Errors and denials

The Assistant's tools surface failures in two distinct places — and they look different.

**Tool-level results.** `execute_integration_tool` never raises to the agent; it returns a
`status`. Treat `needs_credential`, `credit_limit`, `cancelled`, and `error` as normal
control flow the Assistant handles itself.

**Turn-level denials.** The Assistant endpoint that starts a turn is on the billing gate.
A denial there is **not** the `{detail}` shape — it is the flat `DenialEnvelope`:

```json DenialEnvelope (HTTP 402 / 403 / 429) theme={null}
{ "code": "credit_plan_exhausted", "layer": "credit", "key": null, "current": null, "limit": null, "reason": "credit_plan_exhausted" }
```

The `layer` maps to a status: `rate` → **429** (with `Retry-After` and `X-RateLimit-*`
headers), `quota` → **403**, and `credit` or `wallet` → **402**. This is one of the
[four error envelopes](/api-reference/errors) ModuleX returns, and it is emitted on the
run, Composer, Assistant, and managed-knowledge surfaces only — plain CRUD routes use the
`{detail}` shape. See [Errors & status codes](/api-reference/errors) and
[Rate limiting](/api-reference/rate-limiting).

## A worked example: list, then create

This shows the full sequence the Assistant runs to satisfy "create a GitHub issue titled
'Bug' in octocat/hello-world", calling the API directly. Authenticate every request with
`Authorization: Bearer mx_live_…` and `X-Organization-ID` — see
[Authentication](/api-reference/authentication). Owner or admin role is required for the
Assistant; the retired `member` role cannot use it (see
[Roles & permissions](/security/roles-permissions)).

<Steps>
  <Step title="Start the turn">
    Post the user message. The response returns the chat id, the run id, and the SSE
    `stream_url` to open for live events. The operation is shown once below as cURL, Python,
    and JavaScript.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.modulex.dev/assistant/chat \
        -H "Authorization: Bearer mx_live_8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c" \
        -H "X-Organization-ID: a1b2c3d4-e29b-41d4-a716-446655440000" \
        -H "Content-Type: application/json" \
        -d '{
          "message": "Create a GitHub issue titled '\''Bug'\'' in octocat/hello-world",
          "llm": { "integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o" }
        }'
      ```

      ```python Python theme={null}
      import httpx

      resp = httpx.post(
          "https://api.modulex.dev/assistant/chat",
          headers={
              "Authorization": "Bearer mx_live_8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c",
              "X-Organization-ID": "a1b2c3d4-e29b-41d4-a716-446655440000",
          },
          json={
              "message": "Create a GitHub issue titled 'Bug' in octocat/hello-world",
              "llm": {"integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o"},
          },
      )
      start = resp.json()  # {"status": "running", "chat_id": ..., "run_id": ..., "stream_url": ...}
      ```

      ```javascript JavaScript theme={null}
      const resp = await fetch("https://api.modulex.dev/assistant/chat", {
        method: "POST",
        headers: {
          Authorization: "Bearer mx_live_8f3a2b1c9d0e4f5a6b7c8d9e0f1a2b3c",
          "X-Organization-ID": "a1b2c3d4-e29b-41d4-a716-446655440000",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          message: "Create a GitHub issue titled 'Bug' in octocat/hello-world",
          llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" },
        }),
      });
      const start = await resp.json();
      // { status: "running", chat_id, run_id, stream_url }
      ```
    </CodeGroup>
  </Step>

  <Step title="The Assistant discovers and reads the action">
    Over the stream you see the Assistant call `get_available_integrations` (with
    `query="github"`) and then `get_integration_details("github", "create_issue")` to read
    the parameter schema. These are read-only and never pause.
  </Step>

  <Step title="The Assistant executes">
    The Assistant calls `execute_integration_tool` with the action and parameters. Because
    `create_issue` is not destructive, it runs immediately on the Assistant surface — no
    approval prompt. The tool result arrives in the stream:

    ```json tool_result theme={null}
    {
      "status": "ok",
      "integration_name": "github",
      "tool_name": "create_issue",
      "result": { "success": true, "issue": { "number": 42, "title": "Bug", "state": "open" } }
    }
    ```

    Had no GitHub credential existed, the result would instead be
    `{"status": "needs_credential", ...}`, and the Assistant would call `request_credential`
    and pause — see the resume steps under [Human-in-the-loop](/assistant/human-in-the-loop).
  </Step>
</Steps>

<MediaEmbed id="MX-MEDIA-3241" type="app_video" caption={"A short screen recording of the Assistant connecting an integration mid-task via a credential request."} />

## Related

<CardGroup cols={2}>
  <Card title="Human-in-the-loop" icon="hand" href="/assistant/human-in-the-loop">
    The pause-and-resume contract behind credential requests and approvals.
  </Card>

  <Card title="Integrations overview" icon="plug" href="/integrations/overview">
    Connect the services the Assistant calls and manage their credentials.
  </Card>

  <Card title="How the Assistant works" icon="route" href="/assistant/how-it-works">
    The agentic loop that decides when to discover, call a tool, and finish.
  </Card>

  <Card title="Permissions & limits" icon="lock" href="/assistant/permissions-and-limits">
    Who can use the Assistant and the billing and usage limits that apply.
  </Card>
</CardGroup>
