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

# AI Composer: text to workflow

> How the AI Composer turns a plain-English request into an editable workflow graph on the canvas, pauses to ask you questions, and lets you save or revert its changes.

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 **AI Composer** is the text-to-workflow agent in ModuleX. You describe what you want in plain English, and it builds an editable [workflow](/concepts/workflows-and-runs) on the [canvas](/workflow-builder/canvas) for you — adding nodes, wiring them together, and setting up inputs as it goes.

You stay in control the whole time. The Composer pauses to ask you questions when it needs a decision or a credential, streams its work live, and never makes its changes permanent until you approve them.

<CardGroup cols={2}>
  <Card title="Build by describing" icon="wand-magic-sparkles">
    Type a request like "summarize new support emails and post them to Slack" and watch the workflow take shape on the canvas.
  </Card>

  <Card title="You approve every change" icon="circle-check">
    Composer edits are pending until you save them. Don't like the result? Revert in one click and the workflow returns to where it was.
  </Card>
</CardGroup>

<MediaEmbed id="MX-MEDIA-1110" type="app_video" caption={"The Composer generating a workflow from a single prompt, end to end."} />

## What the Composer does

The Composer is an AI agent that edits a workflow's graph for you. Instead of dragging nodes onto the canvas yourself, you tell it the outcome you want and it assembles the steps.

As it works, it can:

<CardGroup cols={2}>
  <Card title="Add and connect nodes" icon="diagram-project">
    It adds any of the nine [node types](/workflow-builder/nodes/overview) — for example an [LLM node](/workflow-builder/nodes/llm), a [tool node](/workflow-builder/nodes/tool), or a [knowledge node](/workflow-builder/nodes/knowledge) — and wires them in the right order.
  </Card>

  <Card title="Set up inputs" icon="sliders">
    It defines the inputs your workflow accepts so the steps have the data they need to run.
  </Card>

  <Card title="Use your integrations" icon="plug">
    It looks at the [integrations](/integrations/overview) your organization has connected and picks the right tools for the job.
  </Card>

  <Card title="Test as it builds" icon="flask">
    It can do a test run of the workflow while building so it can catch problems early.
  </Card>
</CardGroup>

The Composer edits the workflow live, but those edits are held as **pending changes** until you decide what to do with them (see [Save and revert](#save-and-revert)).

<Card title="See it in the builder" icon="pen-ruler" href="/workflow-builder/composer">
  This page explains the concept. To use the Composer hands-on inside the canvas, see AI Composer in the builder.
</Card>

## Composer vs Assistant

ModuleX has two AI agents, and they are easy to mix up. They share the same underlying chat engine, but they do different jobs.

|                               | **AI Composer**                                    | **[Assistant](/concepts/assistant)** |
| ----------------------------- | -------------------------------------------------- | ------------------------------------ |
| What it produces              | An editable workflow on the canvas                 | Answers and actions, in a chat       |
| Works on a workflow?          | Yes — it builds and edits one                      | No — it has no workflow tools        |
| Where you find it             | The [workflow builder](/workflow-builder/overview) | [Chat](/platform/chat/overview)      |
| Calls your integration tools? | While building and test-running                    | Directly, to get a task done         |
| Save / revert changes         | Yes (pending changes on the canvas)                | Not applicable — no canvas to change |

<Tip>
  Use the **Composer** when you want to *build something reusable* you can run again and again. Use the **[Assistant](/concepts/assistant)** when you want to *get a one-off task done now* in a conversation.
</Tip>

Both agents pause the same way when they need your input, which is covered next.

## When the Composer asks you a question

The Composer doesn't guess on important decisions. When it needs a choice, a piece of text, or access to a connected service, it pauses and asks. This is called **human-in-the-loop** (HITL).

While it is waiting, the conversation stays open and nothing changes until you respond. You can also skip most questions if you'd rather the Composer decide.

<AccordionGroup>
  <Accordion title="A choice between options" icon="list-check">
    The Composer offers a short list of options and you pick one (or several). For example, choosing which model provider to use for a step.
  </Accordion>

  <Accordion title="A yes or no" icon="thumbs-up">
    A simple confirmation before it does something — for instance, before running a sensitive action while test-building.
  </Accordion>

  <Accordion title="Some free text" icon="keyboard">
    A short written answer, such as the exact wording for a message or the name of a channel.
  </Accordion>

  <Accordion title="A credential for an integration" icon="key">
    If a step needs a service you haven't connected yet, the Composer asks you to connect it. For services that use OAuth, this opens the provider's sign-in flow, and the Composer **continues on its own** as soon as you finish — you don't need to re-send your request.
  </Accordion>
</AccordionGroup>

<Note>
  Only one question can be open per conversation at a time, and only the person who was asked can answer it. If you start a new request while a question is still open, the Composer asks you to answer the open one first.
</Note>

<MediaEmbed id="MX-MEDIA-1111" type="screenshot" caption={"A Composer human-in-the-loop question rendered inline in the chat."} />

For the developer-facing detail on how pause-and-resume works over the API, see [Human-in-the-loop resume](/realtime/hitl). The same idea also exists as a workflow step you can place yourself — the [Interrupt node](/workflow-builder/nodes/interrupt).

## Save and revert

Composer changes are never silently committed. After the Composer finishes a turn, its edits sit on the canvas as **pending changes**, and an action bar gives you two choices.

<Steps>
  <Step title="Describe what you want">
    Send your request. The Composer streams its work and applies edits to the canvas as it goes.
  </Step>

  <Step title="Review the pending changes">
    When the turn finishes, the workflow shows the Composer's edits with a pending-changes action bar. Nothing is locked in yet.
  </Step>

  <Step title="Save or revert">
    **Save** keeps the changes and updates the workflow's saved snapshot. **Revert** restores the workflow to its last saved snapshot and discards the edits.
  </Step>
</Steps>

<CardGroup cols={2}>
  <Card title="Save" icon="floppy-disk">
    Confirms the Composer's edits and refreshes the saved snapshot of your workflow. This becomes the new baseline for the next revert.
  </Card>

  <Card title="Revert" icon="rotate-left">
    Restores the workflow to the snapshot from before this turn and clears the pending changes — a clean undo of the Composer's latest edits.
  </Card>
</CardGroup>

<Tip>
  You can keep iterating without committing: send another request, see the new result, and only save once you're happy. Each save sets the baseline that a later revert will return to.
</Tip>

## Building a turn programmatically

Most people use the Composer through the [builder](/workflow-builder/composer). If you're working from code, a Composer turn starts with a single call that creates or continues a chat and kicks off the agent. The response gives you a stream URL you can follow to watch the build live.

Every request needs your API key and your organization context — see [Authentication](/api-reference/authentication).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.modulex.dev/composer/chat \
    -H "Authorization: Bearer mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF" \
    -H "X-Organization-ID: 7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41" \
    -H "Content-Type: application/json" \
    -d '{
      "workflow_id": "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b",
      "message": "Add an LLM node that summarizes the input text",
      "llm": {
        "integration_name": "anthropic",
        "provider_id": "anthropic",
        "model_id": "claude-haiku-3.5"
      }
    }'
  ```

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

  client = Modulex(
      api_key="mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF",
      organization_id="7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41",
  )

  turn = await client.composer.chat(
      "Add an LLM node that summarizes the input text",
      workflow_id="a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b",
      llm={
          "integration_name": "anthropic",
          "provider_id": "anthropic",
          "model_id": "claude-haiku-3.5",
      },
  )
  print(turn.stream_url)
  ```

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

  const client = new Modulex({
    apiKey: "mx_live_8Kqp2wZ7nR4tVx1aB6cD3eF",
    organizationId: "7f3a2b10-9c4d-4e8a-b1f6-2d5c8e0a9b41",
  });

  const turn = await client.composer.chat({
    workflowId: "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b",
    message: "Add an LLM node that summarizes the input text",
    llm: {
      integration_name: "anthropic",
      provider_id: "anthropic",
      model_id: "claude-haiku-3.5",
    },
  });
  console.log(turn.stream_url);
  ```
</CodeGroup>

A successful response confirms the turn is running and tells you where to stream it:

```json Response theme={null}
{
  "status": "running",
  "composer_chat_id": "f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73",
  "workflow_id": "a1b2c3d4-5e6f-4708-9a1b-2c3d4e5f6a7b",
  "run_id": "8e7d6c5b-4a39-4218-9f0e-1d2c3b4a5e6f",
  "thread_id": "f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73",
  "stream_url": "/composer/chat/f2c1a890-3b47-4d62-8e15-6a9c0d2e4f73/listen/8e7d6c5b-4a39-4218-9f0e-1d2c3b4a5e6f"
}
```

From there you follow the `stream_url` to watch the agent's steps, answer any human-in-the-loop questions it raises, and then confirm with save or revert.

<Note>
  The Composer runs on managed AI usage, so it consumes [credits](/concepts/credits-billing) — one credit per turn, plus the cost of the AI it uses. If your organization is out of credits or over a limit, the call is refused before any work begins with a billing response (a `402`, `403`, or `429`). See [Usage gating & limits](/billing/usage-gating) and [Errors & status codes](/api-reference/errors) for the exact shapes.
</Note>

<Note>
  The Composer is available to organization **owners and admins**. The previous **member** role has been retired, so any account that can open the Composer can also use it. See [Organizations, roles & membership](/concepts/organizations-roles).
</Note>

## Known limitations

A few related behaviors are not fully wired today. We list them here so you don't build a flow that depends on them.

<AccordionGroup>
  <Accordion title="Bulk deleting nodes on a shared canvas" icon="triangle-exclamation">
    Deleting nodes **one at a time** is fully supported and syncs to everyone editing the canvas with you. When collaborating, delete nodes individually — confirming each — rather than in a single bulk action. See [Realtime co-editing & external sync](/workflow-builder/realtime-coediting).
  </Accordion>

  <Accordion title="Reconnecting an expired OAuth integration" icon="triangle-exclamation">
    When the Composer asks for a credential and you connect a service via OAuth, the connect-and-continue flow works. To refresh an expired OAuth credential, reconnect the integration — that re-runs the sign-in flow and issues fresh tokens. See [Managing credentials](/integrations/managing-credentials).
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Use the Composer in the builder" icon="pen-ruler" href="/workflow-builder/composer">
    Open the Composer panel on the canvas and build hands-on.
  </Card>

  <Card title="Build a workflow with Composer" icon="map" href="/guides/build-with-composer">
    A step-by-step walkthrough from prompt to running workflow.
  </Card>

  <Card title="Meet the Assistant" icon="comments" href="/concepts/assistant">
    The chat agent that gets one-off tasks done without a workflow.
  </Card>

  <Card title="How human-in-the-loop works" icon="hand" href="/realtime/hitl">
    The developer-facing detail on pausing and resuming a turn.
  </Card>
</CardGroup>
