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

# Versioning, deployments & canvas history

> How ModuleX tracks workflow versions: immutable deployment snapshots, the canvas edit-version history, undo/redo, and restoring a prior version through the API and SDKs.

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 in ModuleX changes along three independent timelines, and it helps to keep them apart:

<CardGroup cols={3}>
  <Card title="Edit version" icon="pen-line">
    The live canvas state. Every accepted edit bumps a monotonic `edit_version` integer and records an RFC-6902 patch of the change.
  </Card>

  <Card title="Deployments" icon="rocket">
    Immutable snapshots of the canvas taken when you deploy. One deployment is marked live; runs from a saved workflow execute the live snapshot, not the draft canvas.
  </Card>

  <Card title="Run history" icon="clock-rotate-left">
    The durable record of executions. Each run row references the deployment it ran (`deployment_id`) plus its input and output summary. See [running workflows](/workflow-builder/execution/running).
  </Card>
</CardGroup>

The draft canvas is what you edit; a deployment is a frozen copy you promote; a run is one execution against a snapshot. Editing the canvas never changes a past deployment or a past run. "Restoring a prior version" means activating an older deployment so future runs use it.

<Note>
  All routes and SDK methods on this page require an **owner** or **admin** role and the auth headers `Authorization: Bearer mx_live_…` plus `X-Organization-ID`. The retired `member` role cannot edit or deploy. See [roles & permissions](/security/roles-permissions) and [authentication](/api-reference/authentication).
</Note>

## The edit version (live canvas)

Each workflow carries an `edit_version` integer, a `last_edited_by` user id, and a `last_edited_at` timestamp. The version advances every time the canvas is saved, whether the edit came from you, a collaborator, the [AI Composer](/workflow-builder/composer), or a [REST `PATCH`/`PUT`](/api-reference/overview). This is the version a collaborative session uses to detect and reconcile concurrent edits.

### Two version planes

The realtime collaboration server and the saved workflow keep two version counters that advance at different rates by design. Understanding the split prevents surprises when you read version numbers off the wire.

| Plane                   | Counter                     | Advances                                                           | Where you see it                                             |
| ----------------------- | --------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------ |
| Room version (Plane A)  | In-memory per open workflow | **+1 per accepted operation** (one node move, one edge connect, …) | the `version` on `ack` and on broadcasts like `node:moved`   |
| Saved version (Plane B) | `edit_version`              | **+1 per save** (one save can batch many operations)               | the `version` on the `saved` event and `GET /workflows/{id}` |

The room batches accepted operations and saves them roughly every two seconds. One save can carry many operations, so after N edits and one save the room version is `base + N` while `edit_version` is `base + 1`. Both numbers are correct; they measure different things.

<Warning>
  Do not overwrite your tracked client version from the `saved` event. `saved.version` is the saved `edit_version` (Plane B) and can be numerically **behind** the `ack`/broadcast `version` (Plane A) you have been following. Treat `saved` as a persistence confirmation, not a version source. Full event semantics live in [presence, locks & versioning](/realtime/presence-locks) and [Socket.io collaboration events](/realtime/socket-events).
</Warning>

### Edit conflicts

When a client submits an operation whose version is too far behind the room (more than the server's tolerance window), the server rejects it and emits a `conflict` event instead of applying it:

```json theme={null}
{ "yourVersion": 5, "serverVersion": 7, "resolution": "rebase" }
```

`resolution` is always the literal string `rebase`. The server does not perform a rebase for you — the value is advisory. The client's recovery is to leave and re-join the workflow to resynchronize from the authoritative state. A separate, **silent** conflict can occur at save time if an external writer (Composer, a REST `PATCH`, or an activation) advanced `edit_version` past the room's last-saved base; the room re-queues the patches and retries on the next tick without emitting anything to you. Conflicts are covered end to end in [realtime co-editing & external sync](/workflow-builder/realtime-coediting).

## Canvas edit history

Every successful save also records one edit-history entry. Each entry stores the JSON Patch array (RFC-6902) that took the workflow from the previous version to this one; each `edit_version` is unique per workflow.

<ResponseField name="id" type="uuid">
  Unique identifier for the history entry.
</ResponseField>

<ResponseField name="workflow_id" type="uuid">
  The workflow this edit belongs to. Edit history is removed when the workflow is deleted.
</ResponseField>

<ResponseField name="user_id" type="uuid">
  The user who made the edit.
</ResponseField>

<ResponseField name="edit_version" type="integer">
  The sequential version this patch produced. Unique per workflow.
</ResponseField>

<ResponseField name="patches" type="array">
  The RFC-6902 JSON Patch array applied to reach this version, for example `[{op, path, value}]`. Paths address the schema, such as `/workflow/nodes/2/name`.
</ResponseField>

<ResponseField name="created_at" type="datetime">
  When the edit was persisted.
</ResponseField>

This edit history powers the audit trail and the in-app undo/redo, but it is an internal store.

<Warning>
  There is **no public REST endpoint or SDK method to read, replay, or revert the edit history.** It is written by the collaboration server and the Composer; it is not exposed for retrieval. Undo/redo in the builder is a client-side snapshot stack (see below), and rolling back to an earlier point is done through **deployments**, not by replaying patches. Do not build automation that assumes a history-read API exists. This gap is tracked in [known limitations](/reference/known-limitations).
</Warning>

### Undo and redo in the canvas

Undo/redo in the builder is a client-only feature, not a server operation. The canvas keeps a bounded stack of shallow snapshots (up to 50). Undo and redo compute the difference between two snapshots — added/deleted nodes, connected/disconnected edges, moved nodes — and push that diff to the collaboration server as a single batched operation so other collaborators converge. Undo does not call any `/workflows` route directly and does not touch deployments. Closing the canvas clears the stack.

## Deployments (versioned snapshots)

A deployment is an immutable copy of the workflow taken at deploy time. Deploying captures the current canvas (`workflow_schema`), name, description, version, default input, and execution config into a new `WorkflowDeployment` row, then marks it live. The draft canvas keeps evolving afterward; the deployment does not.

Deployments are the unit of versioning and rollback. They matter because **a run from a saved workflow executes the live deployment, not the draft canvas**:

* `POST /workflows/run` with a `workflow_id` loads the schema from that workflow's **live** deployment.
* If the workflow has no live deployment, the run is rejected with **`400`** and the message `Workflow has no active deployment. Deploy the workflow first using POST /workflows/{workflow_id}/deploy`.
* An ad-hoc run that sends an inline `workflow` schema (what the builder's **Run** button does) bypasses deployments entirely and runs the canvas as-is.

See [running workflows](/workflow-builder/execution/running) and [run via API](/workflow-builder/execution/api-endpoint) for the execution side, and [schedules](/workflow-builder/execution/schedule) — a schedule also requires a live deployment.

### Deployment record

<ResponseField name="id" type="uuid">
  Unique deployment identifier. Use this id to get, activate, or delete the deployment.
</ResponseField>

<ResponseField name="workflow_id" type="uuid">
  The parent workflow. Omitted from list-row responses; present on the detail response.
</ResponseField>

<ResponseField name="name" type="string">
  Workflow name captured at deploy time.
</ResponseField>

<ResponseField name="version" type="string">
  Snapshot version string. On each deploy the major version auto-bumps, for example `1.0.0` → `2.0.0` → `3.0.0`. Defaults to `1.0.0`.
</ResponseField>

<ResponseField name="deployment_note" type="string | null">
  Optional note you pass at deploy time, for example `Bug fix for empty-query edge case`.
</ResponseField>

<ResponseField name="schema_image_url" type="string | null">
  Optional URL of a visual snapshot of the canvas, generated from the React Flow graph.
</ResponseField>

<ResponseField name="deployed_by" type="uuid | null">
  The user who deployed. Nullable — the deployment persists even if that user is later removed.
</ResponseField>

<ResponseField name="is_live" type="boolean">
  Whether this deployment is the one the workflow currently runs. Exactly one deployment is live at a time (or none, after deactivation).
</ResponseField>

<ResponseField name="created_at" type="datetime">
  When the deployment was created.
</ResponseField>

The detail response (`GET …/{deployment_id}`) additionally includes `description`, the full `workflow_schema`, the snapshotted `input`, and the snapshotted `config`. List rows omit `workflow_id`, `description`, and the schema to stay light.

### Deploy a workflow

`POST /workflows/{workflow_id}/deploy` snapshots the current canvas and immediately marks the new deployment live.

<ParamField path="workflow_id" type="string" required>
  The UUID of the workflow to deploy.
</ParamField>

<ParamField body="deployment_note" type="string">
  Optional note describing this deployment.
</ParamField>

<ParamField body="schema_image_url" type="string">
  Optional URL of a visual snapshot of the canvas.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deploy \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111" \
    -H "Content-Type: application/json" \
    -d '{"deployment_note": "Ship lead-routing v2"}'
  ```

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


  async def main():
      async with Modulex(
          api_key="mx_live_xxx",
          organization_id="11111111-1111-1111-1111-111111111111",
      ) as client:
          deployment = await client.deployments.create(
              "550e8400-e29b-41d4-a716-446655440000",
              deployment_note="Ship lead-routing v2",
          )
          print(deployment.id, deployment.version, deployment.is_live)


  asyncio.run(main())
  ```

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

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

  const deployment = await client.deployments.create(
    "550e8400-e29b-41d4-a716-446655440000",
    { deploymentNote: "Ship lead-routing v2" },
  );

  console.log(deployment.id, deployment.version, deployment.is_live);
  ```
</CodeGroup>

The response is the deployment record with `is_live: true`. Errors: **`400`** `Invalid workflow_id format`, **`404`** `Workflow not found`, **`500`** on an internal failure.

### List deployments

`GET /workflows/{workflow_id}/deployments` returns the deployment history, newest first.

<ParamField query="limit" type="integer" default="20">
  Page size, 1–100.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  Number of rows to skip. Pagination is offset-based; see [pagination](/api-reference/pagination).
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments?limit=20 \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111"
  ```

  ```python Python theme={null}
  deployments = await client.deployments.list(
      "550e8400-e29b-41d4-a716-446655440000",
      limit=20,
      offset=0,
  )
  for d in deployments.deployments:
      print(d.version, d.is_live, d.deployment_note)
  ```

  ```javascript JavaScript theme={null}
  const deployments = await client.deployments.list(
    "550e8400-e29b-41d4-a716-446655440000",
    { limit: 20, offset: 0 },
  );

  for (const d of deployments.deployments) {
    console.log(d.version, d.is_live, d.deployment_note);
  }
  ```
</CodeGroup>

The response is `{ deployments: [...], total, limit, offset }`. The `is_live` flag marks the active deployment.

### Restore a prior version (activate a deployment)

Restoring an earlier version means **activating** an older deployment so future runs use its snapshot. Activation does not touch the draft canvas — your editable workflow stays as it is; only `live_deployment_id` moves.

`PUT /workflows/{workflow_id}/deployments/{deployment_id}/activate`

<Steps>
  <Step title="Find the deployment to restore">
    List the deployments and pick the `id` of the version you want live (use `version`, `created_at`, and `deployment_note` to identify it).
  </Step>

  <Step title="Activate it">
    Call `activate` with that deployment id. The previously live deployment is recorded in the response so you can swap back if needed.
  </Step>

  <Step title="Verify">
    Subsequent saved-workflow runs now execute the restored snapshot. The draft canvas is unchanged.
  </Step>
</Steps>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PUT https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/7c9e6679-7425-40de-944b-e07fc1f90ae7/activate \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111"
  ```

  ```python Python theme={null}
  result = await client.deployments.activate(
      "550e8400-e29b-41d4-a716-446655440000",
      "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  )
  print(result.success, result.previous_live_deployment_id)
  ```

  ```javascript JavaScript theme={null}
  const result = await client.deployments.activate(
    "550e8400-e29b-41d4-a716-446655440000",
    "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  );

  console.log(result.success, result.previous_live_deployment_id);
  ```
</CodeGroup>

The response is `{ success: true, message, deployment_id, previous_live_deployment_id? }`. If the deployment is already live, you get `{ success: true, message: "Deployment is already live", deployment_id }`. Errors: **`400`** invalid UUID, **`404`** workflow or deployment not found.

<Tip>
  A clean rollback workflow: keep deploying as you ship (each deploy auto-bumps the major version and becomes live), and if a release misbehaves, `activate` the previous deployment id from the list response's `previous_live_deployment_id` to revert in one call.
</Tip>

### Get a single deployment

`GET /workflows/{workflow_id}/deployments/{deployment_id}` returns the full record, including the stored `workflow_schema`, `input`, and `config` — useful for diffing a deployment against the current canvas or against another deployment.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111"
  ```

  ```python Python theme={null}
  detail = await client.deployments.get(
      "550e8400-e29b-41d4-a716-446655440000",
      "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  )
  print(detail.version, detail.workflow_schema)
  ```

  ```javascript JavaScript theme={null}
  const detail = await client.deployments.get(
    "550e8400-e29b-41d4-a716-446655440000",
    "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  );

  console.log(detail.version, detail.workflow_schema);
  ```
</CodeGroup>

### Deactivate the live deployment

`DELETE /workflows/{workflow_id}/deployments/live` clears `live_deployment_id` so no deployment is active. After this, a saved-workflow run returns the **`400`** "no active deployment" error until you deploy or activate again. The path segment `live` is literal.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/live \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111"
  ```

  ```python Python theme={null}
  result = await client.deployments.deactivate(
      "550e8400-e29b-41d4-a716-446655440000",
  )
  print(result.success, result.previous_live_deployment_id)
  ```

  ```javascript JavaScript theme={null}
  const result = await client.deployments.deactivate(
    "550e8400-e29b-41d4-a716-446655440000",
  );

  console.log(result.success, result.previous_live_deployment_id);
  ```
</CodeGroup>

The response is `{ success: true, message, previous_live_deployment_id? }`, or `{ success: true, message: "No live deployment was active" }` when nothing was live.

### Delete a deployment

`DELETE /workflows/{workflow_id}/deployments/{deployment_id}` permanently removes a snapshot. If you delete the live deployment, ModuleX auto-promotes the previous one by `created_at`; if none remains, `live_deployment_id` is cleared.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111"
  ```

  ```python Python theme={null}
  result = await client.deployments.delete(
      "550e8400-e29b-41d4-a716-446655440000",
      "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  )
  print(result.was_live, result.new_live_deployment_id)
  ```

  ```javascript JavaScript theme={null}
  const result = await client.deployments.delete(
    "550e8400-e29b-41d4-a716-446655440000",
    "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  );

  console.log(result.was_live, result.new_live_deployment_id);
  ```
</CodeGroup>

The response is `{ success: true, message, deleted_deployment_id, was_live, new_live_deployment_id? }`. The `new_live_deployment_id` field is populated only when `was_live` is true.

<MediaEmbed id="MX-MEDIA-3040" type="app_video" caption={"Deploying a workflow and then rolling back to a prior deployment from the deployment-history list."} />

## SDK and REST parity

Every deployment operation exists in both SDKs and as a REST route. There is no SDK shortcut that reads or reverts the edit history, because no such endpoint exists.

| Operation                     | REST                                               | JavaScript               | Python                   |
| ----------------------------- | -------------------------------------------------- | ------------------------ | ------------------------ |
| Deploy (snapshot + make live) | `POST /workflows/{id}/deploy`                      | `deployments.create`     | `deployments.create`     |
| List deployments              | `GET /workflows/{id}/deployments`                  | `deployments.list`       | `deployments.list`       |
| Get one deployment            | `GET /workflows/{id}/deployments/{depId}`          | `deployments.get`        | `deployments.get`        |
| Restore / activate            | `PUT /workflows/{id}/deployments/{depId}/activate` | `deployments.activate`   | `deployments.activate`   |
| Deactivate live               | `DELETE /workflows/{id}/deployments/live`          | `deployments.deactivate` | `deployments.deactivate` |
| Delete a deployment           | `DELETE /workflows/{id}/deployments/{depId}`       | `deployments.delete`     | `deployments.delete`     |

See the full [SDK ⇄ API parity matrix](/sdks/parity) and the [deploy & versions](/workflow-builder/execution/deploy) page.

## Credits and the billing gate

Versioning operations do **not** consume credits and are **not** subject to the billing-admission gate. Deploying, listing, activating, deactivating, and deleting deployments are plain CRUD-style routes: on auth or validation failure they return the standard FastAPI envelope `{detail}` (for example a `400` or `404`), not the flat `DenialEnvelope`.

The billing gate applies when you **run** a workflow, not when you version it. A run can return a `402`, `403`, or `429` `DenialEnvelope` with the shape `{code, layer, key, current, limit, reason}`. That behavior is documented on [usage gating & limits](/billing/usage-gating) and [errors & status codes](/api-reference/errors); resuming an interrupted run reuses the run reservation and is not charged again.

## Versioning, history & runs at a glance

<Accordion title="Which version number am I looking at?">
  The room version (Plane A) advances per operation and appears on `ack` and broadcasts. The saved `edit_version` (Plane B) advances per save and appears on the `saved` event and on `GET /workflows/{id}`. A deployment's `version` is a separate string (`1.0.0`, `2.0.0`, …) that auto-bumps on each deploy. Run history exposes none of these as a counter — it records `deployment_id` per run instead.
</Accordion>

<Accordion title="How do I revert a workflow?">
  Activate an older deployment with `deployments.activate` (or the activate REST route). This changes only which snapshot future runs use; it does not rewrite your draft canvas. There is no API to roll the canvas itself back to an earlier `edit_version` — edit history is not exposed for replay.
</Accordion>

<Accordion title="Where do I see past executions?">
  Run history is separate from versioning. Use `GET /workflow-runs` (and `GET /workflow-runs/{run_pk}` for one run) to list durable runs, each of which carries the `deployment_id` it executed and an `output_summary`. See [running workflows](/workflow-builder/execution/running).
</Accordion>

<Accordion title="Why did a teammate's change not appear, or appear as a conflict?">
  Live edits flow over Socket.io. If your client version drifted too far behind the room, the server emits a `conflict` (with `resolution: "rebase"`) and you re-join to resync. External changes from the Composer or a REST `PATCH` are pushed over `workflow:external-sync`. See [realtime co-editing & external sync](/workflow-builder/realtime-coediting) and [presence, locks & versioning](/realtime/presence-locks).
</Accordion>

## Next steps

<CardGroup cols={2}>
  <Card title="Deploy & versions" icon="rocket" href="/workflow-builder/execution/deploy">
    The deployment lifecycle from the builder, end to end.
  </Card>

  <Card title="Presence, locks & versioning" icon="users" href="/realtime/presence-locks">
    How the collaboration version planes, locks, and conflicts work on the wire.
  </Card>

  <Card title="Running workflows" icon="play" href="/workflow-builder/execution/running">
    Run a workflow and read its durable run history.
  </Card>

  <Card title="Realtime co-editing & external sync" icon="arrows-rotate" href="/workflow-builder/realtime-coediting">
    How canvas edits and external changes converge across collaborators.
  </Card>
</CardGroup>
