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

# Deploy & version a workflow

> Create immutable deployment snapshots of a workflow, manage versions, and promote or roll back the live deployment from the app, the REST API, and the 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 **deployment** is an immutable snapshot of a workflow taken at the moment you deploy it. Each deployment freezes the workflow's schema, default input, execution config, name, description, and version so that runs use a stable, reviewed copy rather than whatever happens to be on the canvas right now. The deployment you mark as **live** is the one the API runs when you trigger the workflow by `workflow_id`.

This page covers the full deployment lifecycle: creating a deployment, how versions are assigned, promoting an older deployment back to live (rollback), and deleting deployments. Every operation is shown for the app, the REST API, and both SDKs, with complete parameter, response, and error references.

<Note>
  Deployments are why a workflow can be edited continuously while production runs stay stable. Editing the canvas changes the workflow's working schema; it does **not** change any deployment. Runs that load by `workflow_id` always use the live deployment's frozen snapshot until you deploy again or promote a different deployment. For the canvas edit-history side of this story, see [versioning & history](/workflow-builder/versioning-history).
</Note>

## How deployments fit into running a workflow

When you trigger a run by `workflow_id` — from the builder's run control, a [schedule](/workflow-builder/execution/schedule), a [chat trigger](/workflow-builder/execution/run-on-chat), a [published MCP tool](/api-reference/mcp/overview), or the [API](/workflow-builder/execution/api-endpoint) — the engine loads the schema from the workflow's **live deployment**, not from the live canvas. If a workflow has no live deployment, a run-by-id request fails with `400` and the message `Workflow has no active deployment. Deploy the workflow first using POST /workflows/{workflow_id}/deploy`.

There is one exception: an **ad-hoc** run (the builder's "Run" on the current canvas, or an inline `workflow` schema sent to the API) executes the schema you pass in the request and bypasses deployments entirely. Use ad-hoc runs while iterating; deploy when you want a stable target. See [running workflows](/workflow-builder/execution/running) for the run mechanics and [run via API](/workflow-builder/execution/api-endpoint) for the request body.

<MediaEmbed id="MX-MEDIA-3170" type="image" caption={"Diagram showing the relationship between the editable workflow canvas, a stack of immutable deployment snapshots, and the `live_deployment_id` pointer that selects which snapshot runs."} />

### Two different "versions"

Do not confuse the two version counters a workflow carries:

| Concept              | What it is                                                                                                                                           | Where it lives                 | Changes when             |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------ |
| `edit_version`       | A monotonically increasing integer for the live canvas, used for [realtime co-editing](/workflow-builder/realtime-coediting) optimistic concurrency. | The workflow row.              | Every saved canvas edit. |
| Deployment `version` | A semantic-version string (`1.0.0`, `2.0.0`, …) stamped on each immutable snapshot.                                                                  | Each `WorkflowDeployment` row. | Only when you deploy.    |

This page is about the deployment `version`. The `edit_version` is covered under [versioning & history](/workflow-builder/versioning-history) and [presence, locks & versioning](/realtime/presence-locks).

## Permissions and authentication

Every deployment route requires the caller to be an **organization owner or admin**. The retired `member` role cannot deploy, promote, roll back, or delete deployments. See [roles & permissions](/security/roles-permissions) for the role model.

All API and SDK calls authenticate the same way as the rest of the API — with a bearer API key plus the organization header, as documented in the [API overview](/api-reference/overview) and [authentication](/api-reference/authentication):

* `Authorization: Bearer mx_live_…`
* `X-Organization-ID: <your org id>`

The base URL is `https://api.modulex.dev` with no version path segment. Requests for a workflow that does not exist in the caller's organization return `404` (cross-tenant ids are indistinguishable from missing ids).

## Credit impact

Deploying, listing, promoting, and deleting deployments are **not** metered and do **not** consume credits. None of the six deployment routes pass through the billing admission gate — only `POST /workflows/run` is credit-charging. This means you will never see a `402`/`403`/`429` `DenialEnvelope` from a deployment call. The billing gate and its `` `{code, layer, key, current, limit, reason}` `` envelope are documented under [usage gating & limits](/billing/usage-gating) and [errors & status codes](/api-reference/errors); they apply when you run a deployed workflow, not when you deploy it.

## Create a deployment

Deploying snapshots the workflow's **current** schema, name, description, default input, and config into a new immutable `WorkflowDeployment` row, then sets that snapshot as the workflow's live deployment automatically (auto-live). The next run-by-id uses it immediately.

<Steps>
  <Step title="Finish your edits on the canvas">
    Build and test the workflow until it behaves the way you want. Use ad-hoc runs to validate; nothing you do on the canvas affects existing deployments.
  </Step>

  <Step title="Deploy">
    Deploy from the builder, or call `POST /workflows/{workflow_id}/deploy`. ModuleX copies the current schema into a new snapshot, assigns the next version, and marks it live.
  </Step>

  <Step title="Verify the live version">
    List the deployments and confirm the `is_live` flag is on the version you expect. Run the workflow by id to confirm it executes the snapshot.
  </Step>
</Steps>

<MediaEmbed id="MX-MEDIA-3171" type="app_video" caption={"Deploying a workflow from the builder and seeing the new live version appear in the deployment history panel."} />

### Request

<ParamField path="workflow_id" type="string" required>
  Path parameter. The UUID of the workflow to deploy. A non-UUID value returns `400 Invalid workflow_id format`. A workflow that is not in your organization returns `404 Workflow not found`.
</ParamField>

<ParamField body="deployment_note" type="string">
  Optional. A free-text note describing this deployment (for example, `Fixed empty-result branch`). Stored on the snapshot and returned by the list and get operations. Omit to leave it `null`.
</ParamField>

<ParamField body="schema_image_url" type="string">
  Optional. URL of a visual snapshot of the workflow graph (the app generates this from the canvas). Stored on the snapshot for display in deployment history. Omit to leave it `null`.
</ParamField>

The request body is optional; sending no body deploys the current schema with a `null` note and image.

### Response

<ResponseField name="id" type="string">
  UUID of the new deployment snapshot.
</ResponseField>

<ResponseField name="workflow_id" type="string">
  UUID of the parent workflow.
</ResponseField>

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

<ResponseField name="version" type="string">
  Semantic version assigned to this snapshot — see [version assignment](#version-assignment) below.
</ResponseField>

<ResponseField name="deployment_note" type="string | null">
  The note you supplied, or `null`.
</ResponseField>

<ResponseField name="schema_image_url" type="string | null">
  The image URL you supplied, or `null`.
</ResponseField>

<ResponseField name="deployed_by" type="string | null">
  UUID of the user who deployed. `null` if that user has since been removed.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO-8601 timestamp of the deployment.
</ResponseField>

<ResponseField name="is_live" type="boolean">
  Always `true` on the create response — a new deployment becomes live immediately.
</ResponseField>

<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":"Fixed empty-result branch"}'
  ```

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

  client = ModuleX(
      api_key="mx_live_xxx",
      organization_id="11111111-1111-1111-1111-111111111111",
  )

  async def main():
      deployment = await client.deployments.create(
          "550e8400-e29b-41d4-a716-446655440000",
          deployment_note="Fixed empty-result branch",
      )
      print(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: "Fixed empty-result branch" },
  );
  console.log(deployment.version, deployment.isLive);
  ```
</CodeGroup>

```json Example response theme={null}
{
  "id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f",
  "workflow_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Research assistant",
  "version": "2.0.0",
  "deployment_note": "Fixed empty-result branch",
  "schema_image_url": null,
  "deployed_by": "7a2b1c0d-9e8f-4a3b-2c1d-0e9f8a7b6c5d",
  "created_at": "2026-06-21T10:00:00.000000+00:00",
  "is_live": true
}
```

### Errors

| Status                             | When                                                                              |
| ---------------------------------- | --------------------------------------------------------------------------------- |
| `400 Invalid workflow_id format`   | The `workflow_id` is not a valid UUID.                                            |
| `401`                              | Missing or invalid bearer token. The response carries `WWW-Authenticate: Bearer`. |
| `403`                              | The caller is authenticated but is not an org owner/admin.                        |
| `404 Workflow not found`           | No workflow with that id in your organization.                                    |
| `500 Failed to deploy workflow: …` | Unexpected server error.                                                          |

<Note>
  These are FastAPI `` `{detail}` `` error envelopes. Deployment routes never return the billing `DenialEnvelope` because they are not credit-gated. The full error-shape taxonomy is on [errors & status codes](/api-reference/errors).
</Note>

## Version assignment

ModuleX assigns the deployment `version` automatically — you do not pass it. On deploy, the engine reads the most recent deployment for the workflow (by creation time) and computes the next version by incrementing the **major** component and zeroing the rest:

* First ever deployment → `1.0.0`
* Next → `2.0.0`
* Next → `3.0.0`, and so on.

If the most recent version string cannot be parsed as `major.minor.patch`, the next deploy falls back to `1.0.0`. Versions are never reused, and minor/patch components are not auto-incremented; every deployment is a major bump. The workflow's own `WorkflowMetadata.version` (default `"1.0"`, editable on the canvas) is a separate, descriptive field and does not drive deployment numbering.

<Warning>
  Deployments are immutable. There is no edit-deployment operation — you cannot change a snapshot's schema, note, or version after the fact. To ship a change, edit the canvas and deploy again, which produces a new versioned snapshot.
</Warning>

## List deployments

List every deployment for a workflow, newest first. Use this to review version history and find the deployment id to promote or delete. The `is_live` flag marks which snapshot currently runs.

<ParamField path="workflow_id" type="string" required>
  Path parameter. UUID of the workflow.
</ParamField>

<ParamField query="limit" type="integer" default="20">
  Maximum number of deployments to return. Range `1`–`100`.
</ParamField>

<ParamField query="offset" type="integer" default="0">
  Number of deployments to skip, for pagination. Minimum `0`. See [pagination](/api-reference/pagination) for the offset model used across the API.
</ParamField>

<ResponseField name="deployments" type="array">
  Deployment summaries, newest first.

  <Expandable title="deployment summary">
    <ResponseField name="id" type="string">UUID of the deployment.</ResponseField>
    <ResponseField name="name" type="string">Workflow name at deploy time.</ResponseField>
    <ResponseField name="version" type="string">Semantic version of the snapshot.</ResponseField>
    <ResponseField name="deployment_note" type="string | null">The deploy-time note, or `null`.</ResponseField>
    <ResponseField name="schema_image_url" type="string | null">Visual snapshot URL, or `null`.</ResponseField>
    <ResponseField name="deployed_by" type="string | null">UUID of the deploying user, or `null`.</ResponseField>
    <ResponseField name="created_at" type="string">ISO-8601 deploy timestamp.</ResponseField>
    <ResponseField name="is_live" type="boolean">`true` for the workflow's current live deployment.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="total" type="integer">
  Total number of deployments for the workflow (ignores `limit`/`offset`).
</ResponseField>

<ResponseField name="limit" type="integer">
  The `limit` that was applied.
</ResponseField>

<ResponseField name="offset" type="integer">
  The `offset` that was applied.
</ResponseField>

<Note>
  List rows are summaries and omit `workflow_id`, `description`, and the full `workflow_schema`. Fetch a single deployment to get the complete snapshot, including its schema, default input, and config.
</Note>

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments?limit=20&offset=0 \
    -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.isLive, d.deploymentNote);
  }
  ```
</CodeGroup>

```json Example response theme={null}
{
  "deployments": [
    {
      "id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f",
      "name": "Research assistant",
      "version": "2.0.0",
      "deployment_note": "Fixed empty-result branch",
      "schema_image_url": null,
      "deployed_by": "7a2b1c0d-9e8f-4a3b-2c1d-0e9f8a7b6c5d",
      "created_at": "2026-06-21T10:00:00.000000+00:00",
      "is_live": true
    },
    {
      "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "name": "Research assistant",
      "version": "1.0.0",
      "deployment_note": "Initial release",
      "schema_image_url": null,
      "deployed_by": "7a2b1c0d-9e8f-4a3b-2c1d-0e9f8a7b6c5d",
      "created_at": "2026-06-20T09:00:00.000000+00:00",
      "is_live": false
    }
  ],
  "total": 2,
  "limit": 20,
  "offset": 0
}
```

### Errors

| Status                              | When                               |
| ----------------------------------- | ---------------------------------- |
| `400 Invalid workflow_id format`    | `workflow_id` is not a UUID.       |
| `404 Workflow not found`            | Workflow not in your organization. |
| `500 Failed to list deployments: …` | Unexpected server error.           |

## Get a single deployment

Fetch one deployment with its complete immutable snapshot — the full `workflow_schema`, default `input`, and execution `config` captured at deploy time. Use this to inspect exactly what a version will run, or to diff two versions client-side.

<ParamField path="workflow_id" type="string" required>
  Path parameter. UUID of the workflow.
</ParamField>

<ParamField path="deployment_id" type="string" required>
  Path parameter. UUID of the deployment.
</ParamField>

<ResponseField name="id" type="string">UUID of the deployment.</ResponseField>
<ResponseField name="workflow_id" type="string">UUID of the parent workflow.</ResponseField>
<ResponseField name="name" type="string">Workflow name at deploy time.</ResponseField>
<ResponseField name="description" type="string | null">Workflow description at deploy time.</ResponseField>
<ResponseField name="version" type="string">Semantic version of the snapshot.</ResponseField>
<ResponseField name="deployment_note" type="string | null">The deploy-time note, or `null`.</ResponseField>
<ResponseField name="schema_image_url" type="string | null">Visual snapshot URL, or `null`.</ResponseField>
<ResponseField name="deployed_by" type="string | null">UUID of the deploying user, or `null`.</ResponseField>
<ResponseField name="created_at" type="string">ISO-8601 deploy timestamp.</ResponseField>
<ResponseField name="is_live" type="boolean">`true` if this is the live deployment.</ResponseField>

<ResponseField name="workflow_schema" type="object">
  The frozen `WorkflowDefinition` (metadata, config, state schema, nodes, edges) the run engine executes. The node and reference model behind this schema is documented under [workflow engine & nodes](/concepts/workflow-engine) and [variables & references](/workflow-builder/variables-and-references).
</ResponseField>

<ResponseField name="input" type="object">
  Default input parameters captured at deploy time. A run-by-id request can override these per call.
</ResponseField>

<ResponseField name="config" type="object">
  Execution config captured at deploy time (for example `recursion_limit`). A run-by-id request's `config` merges over this.
</ResponseField>

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f \
    -H "Authorization: Bearer mx_live_xxx" \
    -H "X-Organization-ID: 11111111-1111-1111-1111-111111111111"
  ```

  ```python Python theme={null}
  deployment = await client.deployments.get(
      "550e8400-e29b-41d4-a716-446655440000",
      "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f",
  )
  print(deployment.version)
  print(deployment.workflow_schema)
  ```

  ```javascript JavaScript theme={null}
  const deployment = await client.deployments.get(
    "550e8400-e29b-41d4-a716-446655440000",
    "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f",
  );
  console.log(deployment.version);
  console.log(deployment.workflowSchema);
  ```
</CodeGroup>

### Errors

| Status                            | When                                          |
| --------------------------------- | --------------------------------------------- |
| `400 Invalid UUID format`         | Either path id is not a UUID.                 |
| `404 Workflow not found`          | Workflow not in your organization.            |
| `404 Deployment not found`        | No deployment with that id for this workflow. |
| `500 Failed to get deployment: …` | Unexpected server error.                      |

## Promote a deployment (rollback)

Promoting sets a chosen deployment as the live version. Because deployments are versioned snapshots, "rollback" and "promote" are the same operation — you point `live_deployment_id` at whichever existing snapshot you want to serve. List deployments, find the version to restore, then activate it. The change takes effect on the next run-by-id; runs already in flight are unaffected.

<Steps>
  <Step title="Find the target deployment">
    List the workflow's deployments and pick the `id` of the version you want live (for example, the last known-good `1.0.0`).
  </Step>

  <Step title="Activate it">
    Activate that deployment from the deployment-history panel, or call `PUT /workflows/{workflow_id}/deployments/{deployment_id}/activate`. The previously live deployment is returned so you can audit the change.
  </Step>

  <Step title="Confirm">
    Re-list deployments and confirm the `is_live` flag moved to your target version, then run by id to verify.
  </Step>
</Steps>

<MediaEmbed id="MX-MEDIA-3172" type="screenshot" caption={"The deployment-history panel showing several versions with one marked live and an \"Activate\" action on an older version."} />

<ParamField path="workflow_id" type="string" required>
  Path parameter. UUID of the workflow.
</ParamField>

<ParamField path="deployment_id" type="string" required>
  Path parameter. UUID of the deployment to make live.
</ParamField>

<ResponseField name="success" type="boolean">`true` on success.</ResponseField>

<ResponseField name="message" type="string">
  `Deployment activated successfully`, or `Deployment is already live` when the target was already live (a no-op).
</ResponseField>

<ResponseField name="deployment_id" type="string">UUID of the now-live deployment.</ResponseField>

<ResponseField name="previous_live_deployment_id" type="string | null">
  UUID of the deployment that was live before this call, or `null` if none was live. Omitted from the already-live no-op response.
</ResponseField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PUT https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d/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",
      "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  )
  print(result.success, result.previous_live_deployment_id)
  ```

  ```javascript JavaScript theme={null}
  const result = await client.deployments.activate(
    "550e8400-e29b-41d4-a716-446655440000",
    "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  );
  console.log(result.success, result.previousLiveDeploymentId);
  ```
</CodeGroup>

```json Example response theme={null}
{
  "success": true,
  "message": "Deployment activated successfully",
  "deployment_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "previous_live_deployment_id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f"
}
```

### Errors

| Status                                 | When                                          |
| -------------------------------------- | --------------------------------------------- |
| `400 Invalid UUID format`              | Either path id is not a UUID.                 |
| `404 Workflow not found`               | Workflow not in your organization.            |
| `404 Deployment not found`             | No deployment with that id for this workflow. |
| `500 Failed to activate deployment: …` | Unexpected server error.                      |

## Deactivate the live deployment

Clearing the live deployment leaves the workflow with **no** live version (`live_deployment_id` becomes null). After this, a run-by-id fails with `400 Workflow has no active deployment…` until you deploy again or promote an existing deployment. Use this to take a workflow out of service without deleting its history.

<ParamField path="workflow_id" type="string" required>
  Path parameter. UUID of the workflow.
</ParamField>

<ResponseField name="success" type="boolean">`true` on success.</ResponseField>

<ResponseField name="message" type="string">
  `Live deployment deactivated`, or `No live deployment was active` when there was nothing to deactivate.
</ResponseField>

<ResponseField name="previous_live_deployment_id" type="string">
  UUID of the deployment that was deactivated. Present only when a live deployment existed.
</ResponseField>

<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.message)
  ```

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

<Warning>
  After deactivating, run-by-id (schedules, chat triggers, and `POST /workflows/run` with a `workflow_id`) will fail with `400` until a deployment is live again. Ad-hoc runs that carry an inline schema are unaffected.
</Warning>

### Errors

| Status                                        | When                               |
| --------------------------------------------- | ---------------------------------- |
| `400 Invalid workflow_id format`              | `workflow_id` is not a UUID.       |
| `404 Workflow not found`                      | Workflow not in your organization. |
| `500 Failed to deactivate live deployment: …` | Unexpected server error.           |

## Delete a deployment

Permanently removes a deployment snapshot. If you delete the **live** deployment, ModuleX automatically promotes the next-most-recent deployment (by creation time) to live; if none remains, the workflow is left with no live deployment. The response reports both what happened.

<ParamField path="workflow_id" type="string" required>
  Path parameter. UUID of the workflow.
</ParamField>

<ParamField path="deployment_id" type="string" required>
  Path parameter. UUID of the deployment to delete.
</ParamField>

<ResponseField name="success" type="boolean">`true` on success.</ResponseField>
<ResponseField name="message" type="string">`Deployment deleted successfully`.</ResponseField>
<ResponseField name="deleted_deployment_id" type="string">UUID of the deleted deployment.</ResponseField>
<ResponseField name="was_live" type="boolean">`true` if the deleted deployment had been the live one.</ResponseField>

<ResponseField name="new_live_deployment_id" type="string | null">
  When `was_live` is `true`: the UUID of the deployment auto-promoted to live, or `null` if no deployment remained. Absent otherwise.
</ResponseField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X DELETE https://api.modulex.dev/workflows/550e8400-e29b-41d4-a716-446655440000/deployments/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d \
    -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",
      "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  )
  print(result.was_live, result.new_live_deployment_id)
  ```

  ```javascript JavaScript theme={null}
  const result = await client.deployments.delete(
    "550e8400-e29b-41d4-a716-446655440000",
    "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  );
  console.log(result.wasLive, result.newLiveDeploymentId);
  ```
</CodeGroup>

```json Example response theme={null}
{
  "success": true,
  "message": "Deployment deleted successfully",
  "deleted_deployment_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "was_live": true,
  "new_live_deployment_id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f"
}
```

### Errors

| Status                               | When                                          |
| ------------------------------------ | --------------------------------------------- |
| `400 Invalid UUID format`            | Either path id is not a UUID.                 |
| `404 Workflow not found`             | Workflow not in your organization.            |
| `404 Deployment not found`           | No deployment with that id for this workflow. |
| `500 Failed to delete deployment: …` | Unexpected server error.                      |

## Endpoint reference

All routes are mounted under the `/workflows` prefix with no API version segment, and all require an org owner/admin. The `deployments/live` route is intentionally declared before the `deployments/{deployment_id}` route so the literal `live` segment is matched first and never captured as a deployment id.

| Operation          | Method + path                                                       | SDK method (JS / Python)        |
| ------------------ | ------------------------------------------------------------------- | ------------------------------- |
| Create deployment  | `POST /workflows/{workflow_id}/deploy`                              | `client.deployments.create`     |
| List deployments   | `GET /workflows/{workflow_id}/deployments`                          | `client.deployments.list`       |
| Get deployment     | `GET /workflows/{workflow_id}/deployments/{deployment_id}`          | `client.deployments.get`        |
| Promote / rollback | `PUT /workflows/{workflow_id}/deployments/{deployment_id}/activate` | `client.deployments.activate`   |
| Deactivate live    | `DELETE /workflows/{workflow_id}/deployments/live`                  | `client.deployments.deactivate` |
| Delete deployment  | `DELETE /workflows/{workflow_id}/deployments/{deployment_id}`       | `client.deployments.delete`     |

Both the [JavaScript SDK](/sdks/javascript) and the [Python SDK](/sdks/python) implement all six operations under the `deployments` resource on the client, at full parity with REST — see the [SDK ⇄ API parity matrix](/sdks/parity). The SDKs accept and return snake\_case fields converted to each language's idiom (for example `is_live` ⇄ `isLive`).

## Common workflows

<AccordionGroup>
  <Accordion title="Ship a change safely">
    Edit the canvas, run ad-hoc to validate, then `deployments.create` to snapshot and go live. Existing runs are unaffected until you deploy; the next run-by-id picks up the new version automatically.
  </Accordion>

  <Accordion title="Roll back a bad release">
    `deployments.list` to find the last known-good version's id, then `deployments.activate` on it. Live traffic moves to the older snapshot on the next run-by-id. The previously live id is returned so you can re-promote it later if needed.
  </Accordion>

  <Accordion title="Take a workflow offline">
    `deployments.deactivate` clears the live pointer. Run-by-id then returns `400 Workflow has no active deployment…` until you deploy or promote again. History is preserved.
  </Accordion>

  <Accordion title="Prune old versions">
    `deployments.delete` removes a snapshot permanently. Deleting the live one auto-promotes the next-most-recent deployment (or leaves none); check `new_live_deployment_id` in the response to see where live landed.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Running workflows" icon="play" href="/workflow-builder/execution/running">
    Run a deployed workflow from the builder and watch the live stream.
  </Card>

  <Card title="Run via API" icon="terminal" href="/workflow-builder/execution/api-endpoint">
    Trigger a deployed workflow by id over REST and the SDKs.
  </Card>

  <Card title="Schedules" icon="clock" href="/workflow-builder/execution/schedule">
    Schedules require a live deployment to run.
  </Card>

  <Card title="Versioning & history" icon="git-branch" href="/workflow-builder/versioning-history">
    The canvas edit-history and `edit_version` model behind deployments.
  </Card>

  <Card title="API overview" icon="book" href="/api-reference/overview">
    Base URLs, request lifecycle, and how every operation is shown three ways.
  </Card>

  <Card title="Roles & permissions" icon="shield" href="/security/roles-permissions">
    Why deploying requires the owner or admin role.
  </Card>

  <Card title="Publish as an MCP tool" icon="upload" href="/api-reference/mcp/overview">
    Expose a deployed workflow to external AI clients through ModuleX MCP.
  </Card>
</CardGroup>
