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

# Assistant: agentic chat with tools and approvals

> The Assistant is ModuleX's workflow-independent agentic chat. It searches your connected tools, calls them step by step, pauses for approval on risky actions, and shares the same chat store as the AI Composer.

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 an agentic chat that gets work done with your connected tools. You describe a task in plain language, and the Assistant decides which tools to call, runs them one step at a time, and pauses to ask you when it needs a decision or an approval. There is no workflow to build first.

<MediaEmbed id="MX-MEDIA-1120" type="app_video" caption={"The Assistant completing a multi-step task end to end in the chat surface."} />

## What the Assistant is

Think of the Assistant as a colleague who already knows how to use every service you have connected. You ask, it acts.

<CardGroup cols={3}>
  <Card title="It uses your tools" icon="wrench">
    The Assistant can search your connected integrations and call their actions directly — for example, list your repositories or send a message — using your organization's stored credentials.
  </Card>

  <Card title="It works step by step" icon="footprints">
    Each turn is an agentic loop: the Assistant reasons, picks a tool, runs it, reads the result, and repeats until the task is done or it needs you.
  </Card>

  <Card title="It pauses when it should" icon="hand">
    Before a destructive action, or when it needs a credential or a decision, the Assistant stops and asks. Nothing risky happens without your say-so.
  </Card>
</CardGroup>

The Assistant is **workflow-independent**: it has no tools to inspect, build, run, or edit a workflow. It exists to answer questions and complete tasks directly. If you want to design a reusable workflow graph instead, that is what the [AI Composer](/concepts/ai-composer) is for.

<Note>
  For the full product walkthrough — capabilities, models, examples, and limits — see the [Assistant overview](/assistant/overview).
</Note>

## What the Assistant can do

<CardGroup cols={2}>
  <Card title="Answer questions" icon="message-circle">
    Plain language-model Q\&A, with no tools required when none are needed.
  </Card>

  <Card title="Search your knowledge" icon="book-open">
    Retrieve answers from your connected knowledge bases. See [Knowledge & RAG](/concepts/knowledge-rag).
  </Card>

  <Card title="Find and call integration tools" icon="plug">
    Discover what you have connected and run a tool action directly. See [Integrations overview](/integrations/overview).
  </Card>

  <Card title="Connect new services" icon="key">
    When a task needs a service you have not connected yet, the Assistant asks you to add the credential — including an OAuth connect flow. See [Credentials & OAuth2](/concepts/credentials-oauth).
  </Card>
</CardGroup>

## How a turn works

Every message you send is one **turn** — one request that the Assistant works on until it finishes or stops to ask you something.

<Steps>
  <Step title="You send a message">
    You type a task. The Assistant starts a run and begins streaming its work back to you in real time.
  </Step>

  <Step title="The Assistant reasons and acts">
    It writes out its thinking, then calls a tool — for example, discovering your integrations or executing a specific action. You see each tool call and its result as it happens.
  </Step>

  <Step title="It pauses if it needs you">
    If the next step is risky, needs a decision, or needs a credential, the Assistant pauses and shows you a question. The turn waits for your answer. See [Approval gates and questions](#approval-gates-and-questions).
  </Step>

  <Step title="It finishes">
    When the task is complete, the Assistant returns its final answer and the run ends.
  </Step>
</Steps>

The Assistant's actions stream live over Server-Sent Events. The same stream powers the in-app chat and the SDKs — see [SSE run streaming](/realtime/sse-streaming) and [Streaming responses](/assistant/streaming).

## Approval gates and questions

The Assistant is built to act, not to ask permission for everything. It runs ordinary, low-risk actions on its own — but it draws a hard line at anything that could change or remove your data, and at anything it cannot do without input from you.

<AccordionGroup>
  <Accordion title="Destructive actions always pause" icon="triangle-alert">
    Any action that looks destructive — deletes, drops, truncates, purges, raw database statements, and similar — always pauses for a yes/no approval before it runs. The Assistant shows you an approval card describing the action; only an explicit **yes** lets it proceed. Secret-looking values in the preview are redacted.
  </Accordion>

  <Accordion title="Routine actions run without a prompt" icon="zap">
    For non-destructive actions, the Assistant just runs them as part of getting your task done. This keeps everyday work fast. (This is the key difference from the Composer, which gates most write actions while it is editing a workflow.)
  </Accordion>

  <Accordion title="It asks when it needs a decision" icon="list">
    When the path forward is ambiguous, the Assistant can ask you a structured question: pick one option, pick several, answer yes/no, or type free text. Your answer steers the rest of the turn.
  </Accordion>

  <Accordion title="It asks for credentials it does not have" icon="key">
    If a task needs a service you have not connected, the Assistant asks you to add the credential. For OAuth services, completing the connect flow resumes the turn automatically — you do not have to re-send your message.
  </Accordion>
</AccordionGroup>

<Note>
  A chat can hold **one pending question at a time**. While the Assistant is waiting on your answer, sending a new message to that chat is rejected until you respond, so a turn can never fork. For the full pause-and-resume model, see [Human-in-the-loop](/assistant/human-in-the-loop) and the [HITL resume reference](/realtime/hitl).
</Note>

## Assistant and Composer share one chat store

The Assistant and the [AI Composer](/concepts/ai-composer) are two surfaces over the **same** chat machinery. Both store their conversations in the shared `ComposerChat` store and reuse the same agent engine, streaming, approval/resume flow, and billing. A single field tells them apart:

| Field               | Assistant                     | AI Composer                           |
| ------------------- | ----------------------------- | ------------------------------------- |
| `kind`              | `assistant`                   | `composer`                            |
| Bound to a workflow | No — never workflow-bound     | Yes — edits a specific workflow graph |
| Workflow tools      | None                          | Builds and edits the graph            |
| What it is for      | Getting tasks done with tools | Designing and editing a workflow      |

Every operation is scoped by `kind`, so an Assistant chat and a Composer chat can never act on each other's conversations. Because both live in the shared store, OAuth connect-and-resume works identically across both surfaces.

## When to use the Assistant vs the Composer

<CardGroup cols={2}>
  <Card title="Use the Assistant" icon="bot" href="/assistant/overview">
    For one-off or ad-hoc tasks you want done now: ask a question, pull data from a tool, search your knowledge, or chain a few tool calls together. Nothing is saved as a reusable graph.
  </Card>

  <Card title="Use the AI Composer" icon="workflow" href="/concepts/ai-composer">
    When you want a **repeatable** automation. The Composer turns a plain-English description into an editable [workflow graph](/concepts/workflow-engine) you can run again, schedule, and call from the API.
  </Card>
</CardGroup>

A simple rule: if you would run it more than once, build it with the Composer. If you just need it handled, ask the Assistant.

## Who can use it, and what it costs

<CardGroup cols={2}>
  <Card title="Access" icon="shield">
    The Assistant requires an organization **owner** or **admin** role. The legacy `member` role is retired and cannot use the Assistant. See [Organizations, roles & membership](/concepts/organizations-roles) and [Roles & permissions](/security/roles-permissions).
  </Card>

  <Card title="Cost" icon="coins">
    Each Assistant turn is metered usage and runs through the billing gate, so it consumes [credits](/concepts/credits-billing). Resuming a paused turn after you answer a question is part of the same turn — you are not charged twice.
  </Card>
</CardGroup>

### When usage is blocked

Because Assistant turns are managed usage, they pass through the live billing admission gate **before** any work starts. If your organization is out of credits, over a limit, or rate-limited, the turn is rejected up front with a structured denial:

| Status | Meaning                                                  |
| ------ | -------------------------------------------------------- |
| `402`  | Out of credits, or wallet/overage cannot cover the turn. |
| `403`  | A plan quota for this usage is exhausted.                |
| `429`  | Too many requests too quickly — slow down and retry.     |

These denials use ModuleX's flat denial envelope (`code`, `layer`, `key`, `current`, `limit`, `reason`), which is different from ordinary validation errors. For the exact shapes and how to handle them, see [Usage gating & limits](/billing/usage-gating), [Permissions & limits](/assistant/permissions-and-limits), and [Errors & status codes](/api-reference/errors).

## Where to go next

<CardGroup cols={3}>
  <Card title="Assistant overview" icon="compass" href="/assistant/overview">
    The full product tour of the Assistant.
  </Card>

  <Card title="Chat overview" icon="messages-square" href="/platform/chat/overview">
    Where you talk to the Assistant in the app.
  </Card>

  <Card title="AI Composer" icon="workflow" href="/concepts/ai-composer">
    Build a reusable workflow from a prompt instead.
  </Card>
</CardGroup>
