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

# Workflows & runs explained

> Understand the difference between a workflow and a run in ModuleX, how a run moves through its lifecycle and status, and the three distinct identifiers that all get called a run id.

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

A **workflow** is the thing you build once. A **run** is what happens each time you press go. Keeping those two ideas separate is the key to understanding everything else on this page — including the three different identifiers that are all, confusingly, called a "run id".

<CardGroup cols={2}>
  <Card title="Workflow" icon="diagram-project">
    The reusable design: the nodes, the connections between them, and the settings you saved. It sits still until you run it.
  </Card>

  <Card title="Run" icon="play">
    One execution of that design with specific inputs. Each run has its own progress, its own result, and its own identifier.
  </Card>
</CardGroup>

## Workflow vs. run

Think of a workflow like a recipe and a run like the meal you cook from it. One recipe, many meals — each meal made with different ingredients and turning out slightly differently.

<CardGroup cols={2}>
  <Card title="A workflow is durable" icon="floppy-disk">
    You edit it in the [workflow builder](/workflow-builder/overview) or with the [AI Composer](/concepts/ai-composer), give it a name, and [deploy](/workflow-builder/execution/deploy) it. It stays put until you change it.
  </Card>

  <Card title="A run is a moment" icon="stopwatch">
    Starting a run takes a snapshot, feeds in your inputs, and executes the [nodes](/workflow-builder/nodes/overview) step by step. When it finishes, the run becomes a record in your history.
  </Card>

  <Card title="One workflow, many runs" icon="layer-group">
    Every time you run from the canvas, [from chat](/workflow-builder/execution/run-on-chat), [from the API](/workflow-builder/execution/api-endpoint), or [on a schedule](/workflow-builder/execution/schedule), you create a new, independent run.
  </Card>

  <Card title="Runs outlive workflows" icon="box-archive">
    If you delete a workflow, its past runs stay in your history so you keep the audit trail. The run record keeps everything except its link back to the (now deleted) workflow.
  </Card>
</CardGroup>

<MediaEmbed id="MX-MEDIA-1090" type="image" caption={"A simple \"one workflow, many runs\" diagram."} />

## What happens during a run

Once you start a run, ModuleX walks through the workflow's nodes in order, passing data from each step to the next. You can watch this happen live, and some workflows pause partway through to ask you something before they continue.

<Steps>
  <Step title="You start the run">
    You provide the inputs and press run. ModuleX checks your plan's [credits and limits](/concepts/credits-billing) first, then begins executing in the background and immediately hands you a **run id** so you can follow along.
  </Step>

  <Step title="Nodes execute and stream">
    Each node runs and writes its result into the run's shared state. Progress streams to you live over [Server-Sent Events](/realtime/sse-streaming), so you see each step as it completes rather than waiting for the whole thing.
  </Step>

  <Step title="The run may pause for you">
    If the workflow includes an [interrupt node](/workflow-builder/nodes/interrupt), the run pauses and asks a question — for example, to approve a draft before it is sent. The run waits for your answer; nothing idles out.
  </Step>

  <Step title="You answer, and it resumes">
    You respond, and the run continues from exactly where it stopped. It keeps the same run id and is never charged twice for the same execution.
  </Step>

  <Step title="The run finishes">
    The run ends in one of a few final states (done, failed, or cancelled) and is saved to your run history, where you can reopen it later to see its inputs and results.
  </Step>
</Steps>

<MediaEmbed id="MX-MEDIA-1091" type="app_video" caption={"A 30–45s screen recording of a run streaming live in the app, including an interrupt pause and resume."} />

## The run lifecycle

A run moves through a predictable set of moves: start it, watch it, optionally inspect or steer it, and let it finish. You rarely think about these as separate actions in the app — but they map directly to what happens behind the scenes and to the [API](/workflow-builder/execution/api-endpoint).

<CardGroup cols={2}>
  <Card title="Start" icon="circle-play">
    Kick off a run from a deployed workflow, an ad-hoc canvas, or a schedule. ModuleX returns right away with a run id while the work continues in the background.
  </Card>

  <Card title="Watch (listen)" icon="satellite-dish">
    Subscribe to the run's live event stream to follow progress in real time. Reconnecting mid-run replays what you missed, so you never lose the thread. See [SSE run streaming](/realtime/sse-streaming).
  </Card>

  <Card title="Resume" icon="forward-step">
    When a run pauses at an interrupt, send your answer to continue it. The run picks up from its saved checkpoint — same run, same id.
  </Card>

  <Card title="Cancel" icon="circle-stop">
    Stop a run that is running or paused. Cancellation is graceful: the current step finishes, then the run stops cleanly between steps.
  </Card>
</CardGroup>

## The status model

Every run carries a status that tells you where it is. The live stream you watch and the saved history record use slightly different words for the same idea, which is worth knowing so nothing surprises you.

| Status        | What it means                                                            |
| ------------- | ------------------------------------------------------------------------ |
| `pending`     | The run is queued and about to start.                                    |
| `running`     | The run is actively executing its nodes.                                 |
| `interrupted` | The run is paused at an interrupt node, waiting for your answer.         |
| `succeeded`   | The run completed successfully.                                          |
| `failed`      | A step errored and the run stopped.                                      |
| `cancelled`   | You (or a teammate) stopped the run.                                     |
| `skipped`     | The run did not execute (for example, a scheduled run that was skipped). |

<Note>
  **One word, two places.** While a run streams live, a successful finish arrives as a `done` event. In your saved run history, that same run is recorded with the status `succeeded`. They describe the same outcome — `done` is the live signal, `succeeded` is the stored label.
</Note>

## The three identifiers all called a "run id"

This is the single most important thing to get right about runs. ModuleX uses the phrase "run id" for **three different identifiers** that serve different jobs. They are not interchangeable, and using one where another is expected leads to a "not found" result.

<Note>
  Reach for the right id for the job: use the **execution run id** to watch, resume, or cancel a live run; use the **run record id** to open a finished run in your history; and the **scheduled-run id** only when you are looking at a schedule's own run log.
</Note>

<CardGroup cols={3}>
  <Card title="1. Execution run id" icon="bolt">
    The live handle for one execution. It is what you watch, resume, and cancel against while a run is in flight.
  </Card>

  <Card title="2. Run record id" icon="database">
    The permanent key for a finished run in your history. You use it to look a completed run back up later.
  </Card>

  <Card title="3. Scheduled-run id" icon="calendar-days">
    The key for one entry in a schedule's own run log. It is separate from both ids above.
  </Card>
</CardGroup>

<AccordionGroup>
  <Accordion title="1. The execution run id — the live handle">
    This is the identifier ModuleX hands back the instant you start a run. It is what every live action keys off:

    * **Watch** the run's live stream (`/workflows/listen/{run_id}`).
    * **Cancel** a run that is still going (`/workflows/cancel/{run_id}`).
    * It is also used to replay a run's recent event history if you reconnect.

    For a workflow run, **resuming after an interrupt keeps the same execution run id** — one logical run, charged once. For the conversational [AI Composer](/concepts/ai-composer) and [Assistant](/concepts/assistant), each turn (and each resume) gets a **new** execution run id, even though the conversation itself keeps one stable thread. So an execution run id is not a stable handle across a whole conversation.
  </Accordion>

  <Accordion title="2. The run record id — the history key">
    Every real run is also written to your durable run history as its own record, and that record has its own identifier — separate from the execution run id above.

    You use the **run record id** to open a single finished run and read back its inputs and results (`/workflow-runs/{run_pk}`). When you list your run history, each row shows *both* ids: the execution run id (the live handle the run used) and the record id (the history key). Pass the record id — not the execution run id — when you fetch one run's detail.
  </Accordion>

  <Accordion title="3. The scheduled-run id — the schedule's own log">
    When a workflow runs [on a schedule](/workflow-builder/execution/schedule), the schedule keeps its **own** log of each time it fired. Every entry in that log has its own identifier, separate again from the two ids above.

    A single scheduled-run entry links the schedule, the workflow it ran, and the execution run id of the actual run it triggered — so you can trace a schedule firing all the way back to the live run it produced. When you open one entry in a schedule's run history, you address it by its scheduled-run id, not by the execution run id it points to.
  </Accordion>
</AccordionGroup>

### Side by side

| Identifier           | What it points to                 | When you use it                                   |
| -------------------- | --------------------------------- | ------------------------------------------------- |
| **Execution run id** | One live execution                | Watch, resume, or cancel a run in flight          |
| **Run record id**    | One row in your run history       | Open a finished run to read its inputs and result |
| **Scheduled-run id** | One entry in a schedule's run log | Inspect a single scheduled firing                 |

<MediaEmbed id="MX-MEDIA-1092" type="image" caption={"A diagram showing the three \"run id\" identities and how they relate."} />

## See it in practice

You do not need to think about these ids in the app — the interface hands you the right one for whatever you are doing. They become visible when you run a workflow programmatically. The example below starts a run (which returns the **execution run id**), then lists your run history (where each row carries both the **execution run id** and the **run record id**).

<CodeGroup>
  ```bash cURL theme={null}
  # 1. Start a run — the response includes the execution run id ("run_id").
  curl -X POST https://api.modulex.dev/workflows/run \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \
    -H "Content-Type: application/json" \
    -d '{"workflow_id":"550e8400-e29b-41d4-a716-446655440000","input":{"query":"AI trends"}}'

  # 2. List run history — each row carries both the execution run id ("run_id")
  #    and the run record id ("id").
  curl https://api.modulex.dev/workflow-runs \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111"
  ```

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

  async with Modulex(
      api_key="mx_live_xxx",
      organization_id="11111111-1111-1111-1111-111111111111",
  ) as client:
      # 1. Start a run — `run.run_id` is the execution run id.
      run = await client.executions.run(
          workflow_id="550e8400-e29b-41d4-a716-446655440000",
          input={"query": "AI trends"},
      )

      # 2. List run history — each row has `run_id` (execution) and `id` (record).
      history = await client.executions.list_runs()
  ```

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

  const client = new Modulex({
    apiKey: "mx_live_xxx",
    organizationId: "11111111-1111-1111-1111-111111111111",
  });

  // 1. Start a run — `run.run_id` is the execution run id.
  const run = await client.executions.run({
    workflowId: "550e8400-e29b-41d4-a716-446655440000",
    input: { query: "AI trends" },
  });

  // 2. List run history — each row has `run_id` (execution) and `id` (record).
  const history = await client.workflowRuns.list();
  ```
</CodeGroup>

<Note>
  Every request uses `Authorization: Bearer mx_live_…` together with the `X-Organization-ID` header. See [authentication](/api-reference/authentication) for how to get a key and find your organization id.
</Note>

## Where to go next

<CardGroup cols={2}>
  <Card title="Workflow engine & nodes" icon="gears" href="/concepts/workflow-engine">
    How nodes, edges, and run state actually fit together under the hood.
  </Card>

  <Card title="SSE run streaming" icon="wave-square" href="/realtime/sse-streaming">
    The live event stream you watch a run with, frame by frame.
  </Card>

  <Card title="Run a workflow" icon="terminal" href="/guides/run-a-workflow">
    A hands-on, end-to-end walkthrough across REST and both SDKs.
  </Card>

  <Card title="Schedules" icon="clock" href="/workflow-builder/execution/schedule">
    Run a workflow automatically on a recurring schedule.
  </Card>
</CardGroup>
