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

# Realtime & collaboration model

> ModuleX has two separate realtime systems: runs stream their progress to you over SSE, and the workflow canvas syncs between teammates over Socket.io. This page explains what each one is for and how they fit together.

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

ModuleX feels live in two different ways. When you run a workflow, you watch each step report its progress in real time. When you build a workflow with a teammate, you see their cursor move and their edits appear on the same canvas as yours. These are two separate systems, and keeping them apart is the whole mental model.

This page gives you that model in plain terms: what each realtime system does, when each one is in play, and what to expect when several people work together. For the wire-level detail, follow the links into [Realtime overview & event taxonomy](/realtime/overview).

## Two realtime planes

Think of ModuleX as having two independent "live" channels that never overlap. One pushes a run's progress *to* you. The other keeps everyone's *canvas* in agreement.

<CardGroup cols={2}>
  <Card title="Run streaming (SSE)" icon="radio" href="#run-streaming-watching-a-run-as-it-happens">
    A one-way feed of a run's progress: the workflow, the AI Composer, or the Assistant reports each step to you as it happens.
  </Card>

  <Card title="Canvas collaboration (Socket.io)" icon="users" href="#canvas-collaboration-building-together-live">
    A two-way sync of the workflow canvas: cursors, presence, locks, and edits flow between everyone in the same workflow.
  </Card>
</CardGroup>

<Note>
  "Realtime" in ModuleX always means one of these two systems. They share no events and no connection. A run streaming to you over SSE is unrelated to your teammate editing the canvas over Socket.io — even when both are happening in the same workflow at the same time.
</Note>

<MediaEmbed id="MX-MEDIA-1170" type="image" caption={"A diagram contrasting the two realtime planes side by side."} />

## Run streaming: watching a run as it happens

When you start a run, ModuleX opens a live feed of that run's progress over **Server-Sent Events (SSE)**. It is a one-way stream: the server sends you events as the run moves forward, and you watch.

You see this feed in three places, and they all work the same way:

<CardGroup cols={3}>
  <Card title="Workflow runs" icon="play" href="/concepts/workflows-and-runs">
    Each node reports when it starts, when it finishes, and what it produced — until the run reaches `done`.
  </Card>

  <Card title="AI Composer" icon="wand-magic-sparkles" href="/concepts/ai-composer">
    The Composer streams its thinking and the changes it makes to your canvas as it builds.
  </Card>

  <Card title="Assistant" icon="robot" href="/concepts/assistant">
    The Assistant streams its response and the tool calls it makes, step by step.
  </Card>
</CardGroup>

A run's stream carries a small set of progress events. You do not need to memorize them, but it helps to recognize the rhythm:

<Steps>
  <Step title="The run starts">
    The first event describes the run: which workflow, which version, and the run's identifier.
  </Step>

  <Step title="Each step reports in">
    As the run progresses, you get an event when a node (or an Assistant tool call) starts, and another when it produces output. If a step fails, you see the failure and any automatic retry.
  </Step>

  <Step title="The run finishes">
    The stream ends with a final event — `done` when the run completes, or `error` if it could not. A run you cancel ends with `cancelled` instead.
  </Step>
</Steps>

<Note>
  The stream is one-way. To act on a run — to cancel it, or to answer a question it asks — you make a separate request; you do not "reply" on the stream. See [SSE run streaming](/realtime/sse-streaming) for the full event list and frame format, and [Human-in-the-loop (HITL) resume](/realtime/hitl) for how a paused run is answered.
</Note>

### When a run pauses to ask you something

Some runs stop and wait for a person. A workflow can include an [interrupt step](/workflow-builder/nodes/interrupt), and the [AI Composer](/concepts/ai-composer) and [Assistant](/concepts/assistant) can pause to ask for a choice, a confirmation, or a credential before continuing. This is **human-in-the-loop (HITL)**.

When this happens, the stream goes quiet — it does not end. The run is waiting. You answer the question (in the app, or with a separate request via an [SDK](/sdks/streaming-hitl)), and the run continues. Because answering restarts the run's progress, you reconnect to a fresh stream to watch the rest.

<Note>
  A paused run holds its place for a while, then expires if no one answers. The exact pause-and-resume mechanics — what kinds of questions a run can ask, and how each is answered — are covered in [Human-in-the-loop (HITL) resume](/realtime/hitl).
</Note>

## Canvas collaboration: building together live

The second realtime system is for the [workflow builder](/workflow-builder/realtime-coediting). When two or more people open the same workflow, ModuleX connects them over **Socket.io** so the canvas stays in sync. Unlike run streaming, this is a two-way conversation: every person both sends their changes and receives everyone else's.

<CardGroup cols={2}>
  <Card title="Presence" icon="eye" href="/realtime/presence-locks">
    You see who else is in the workflow, each with their own color, and their live cursor as they move around the canvas.
  </Card>

  <Card title="Locks" icon="lock" href="/realtime/presence-locks">
    When someone is editing a node, it is locked to them so two people cannot overwrite the same node at once.
  </Card>

  <Card title="Live edits" icon="pen-to-square" href="/workflow-builder/realtime-coediting">
    Adding, moving, connecting, or deleting nodes is broadcast to everyone in the workflow as it happens.
  </Card>

  <Card title="Versioning" icon="code-branch" href="/realtime/presence-locks">
    Each accepted edit advances a version so the server can keep everyone consistent and resolve conflicts.
  </Card>
</CardGroup>

<MediaEmbed id="MX-MEDIA-1171" type="app_video" caption={"Two people co-editing the same workflow canvas in real time."} />

### Who can edit

Collaboration follows the same roles as the rest of ModuleX. Anyone in the [organization](/concepts/organizations-roles) can open a workflow and watch, but editing the canvas requires an **owner or admin**.

<Warning>
  There is **no member role** in ModuleX. A `member` role existed in earlier versions and was retired on 2026-06-20. The live roles are **owner** and **admin** only, and editing the canvas — adding, moving, connecting, or locking nodes — is restricted to those roles. See [Roles & permissions](/security/roles-permissions) for the full map.
</Warning>

### Conflicts are handled for you

When several people edit quickly, two changes can arrive based on slightly different versions of the canvas. ModuleX detects this and asks the affected client to rebase onto the latest version, rather than letting one person silently overwrite another. You do not manage this by hand; the builder keeps everyone converged. The version, lock, and conflict details are in [Presence, locks & versioning](/realtime/presence-locks).

## External sync: when a workflow changes outside the canvas

Not every change to a workflow comes from a person dragging nodes. The [AI Composer](/concepts/ai-composer) edits the workflow on your behalf, and a workflow can also be changed through the API. ModuleX needs everyone with the canvas open to see those changes too.

This is **external sync**. When a workflow is edited from outside the live canvas, that change is delivered to everyone currently viewing the workflow over the same Socket.io collaboration system, using the **`workflow:external-sync`** event. The canvas updates in place, just as it would for a teammate's edit.

<Warning>
  External sync runs over the live **Socket.io `workflow:external-sync`** event — that is the path that keeps the canvas in sync. The event reference is [Socket.io collaboration events](/realtime/socket-events).
</Warning>

<MediaEmbed id="MX-MEDIA-1172" type="app_video" caption={"The AI Composer editing a workflow while a teammate watches the canvas update."} />

## Putting it together

The two systems often run at once, and that is fine — they do not interfere.

<Accordion title="Example: a teammate builds while you run">
  You and a teammate are both in the same workflow. Your teammate adds a node; you see it appear over **canvas collaboration**. Meanwhile you run the workflow; you watch its steps complete over **run streaming**. Two live systems, two purposes, no conflict.
</Accordion>

<Accordion title="Example: the Composer edits while the team watches">
  You ask the [AI Composer](/concepts/ai-composer) to restructure the workflow. The Composer streams its progress to you over **run streaming**, and the edits it makes land on everyone else's canvas over **external sync** on the collaboration system.
</Accordion>

<Accordion title="Example: a run pauses for an answer">
  A run reaches a [human-in-the-loop](/realtime/hitl) step. Its **run stream** goes quiet while it waits. You answer, and a fresh stream picks up where it left off. The canvas, and anyone editing it, is unaffected.
</Accordion>

## Where to go next

<CardGroup cols={2}>
  <Card title="Realtime overview & event taxonomy" icon="diagram-project" href="/realtime/overview">
    The technical map of both planes — every SSE event and every Socket.io event in one place.
  </Card>

  <Card title="Realtime collaboration walkthrough" icon="user-group" href="/guides/realtime-collaboration">
    Invite a teammate and co-edit a workflow live, step by step.
  </Card>

  <Card title="Canvas collaboration" icon="users" href="/platform/collaboration/canvas">
    What co-editing looks like in the app: presence, cursors, and live edits.
  </Card>

  <Card title="Realtime co-editing & external sync" icon="arrows-rotate" href="/workflow-builder/realtime-coediting">
    How canvas edits and external changes sync, in the builder's own terms.
  </Card>
</CardGroup>
