# Managing workflows with the Novu API (/guides/recipes/managing-workflows)

Update workflow definitions in Development with the Novu API, then publish them to other environments through CI/CD.

import { Callout } from 'fumadocs-ui/components/callout';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { ArrowLeftRight, FileSearch, Pencil, PlusCircle, Trash2 } from 'lucide-react';

Use the Novu API to keep workflow definitions aligned with your codebase and promote them across environments. The sections below explain the production flow first, then show how to implement each phase.

<Callout type="info">
  Workflows are scoped to the environment. The same `workflowId` in Development and Production refers to two separate workflow records. Update definitions in Development, then sync when you are ready to publish. For more on environments, refer to [Publishable assets](/platform/developer/environments#publishable-assets).
</Callout>

## How workflow management works in production

Managing workflows in production is a two-phase process. You update workflow definitions in Development from your application code, then you publish those definitions to Staging and Production through CI/CD.

In a real application you usually manage many workflows (for example, `payout-initiated`, `weekly-digest`, and `order-shipped`). The flow is the same for each one: pick a stable `workflowId` per workflow, run the update-first helper for every ID in your list, then sync each ID in CI/CD.

### Phase 1: Update workflow definitions in Development

Store workflow definitions in your repository (steps, payload schema, and channel content). For each `workflowId`, call update first, because that is the path you use on almost every run. Create only when update returns **404** (that workflow does not exist yet in Development).

### Phase 2: Publish workflows to other environments in CI/CD

When definitions in Development are correct, publish them with [Sync a workflow](/api-reference/workflows/sync-a-workflow). Run sync from your CI/CD pipeline (for example, after a merge to `main`) once per `workflowId` so Staging and Production receive the same set of workflows. Use your Development secret key when calling sync. Pass the target environment identifier from the [Novu Dashboard](https://dashboard.novu.co). Each sync copies one workflow from Development into the target environment under the same `workflowId`.

### End-to-end sequence

1. Update workflow definitions in Development for each `workflowId` you manage as code.
2. Publish each workflow from Development to the target environment (Staging first, then Production, if you use both).

## Prerequisites

Install the SDK for your stack and set your secret key from the [Novu Dashboard](https://dashboard.novu.co):

<Tabs items={['Node.js (@novu/api)', 'Python (novu-py)']}>
  <Tab value="Node.js (@novu/api)">
    ```bash
    npm install @novu/api zod
    ```
  </Tab>

  <Tab value="Python (novu-py)">
    ```bash
    pip install novu-py
    ```
  </Tab>
</Tabs>

## Update workflow definitions in Development

This section implements *phase 1*: updating workflow definitions in Development.

For each `workflowId`, try update first and create on **404**:

1. Update the workflow with that `workflowId` and its full definition.
2. If update returns **404**, create a new workflow with the same `workflowId`.

Keep a list of every `workflowId` you manage as code (a constant array, a registry map, or exports from workflow modules) and loop over it in your deploy script or bootstrap job.

<Tabs items={['Node.js', 'Python']}>
  <Tab value="Node.js">
    ```typescript
    import { Novu } from "@novu/api";

    const novu = new Novu({
      secretKey: process.env.NOVU_SECRET_KEY!,
    });

    /** Every workflowId you manage as code. */
    const MANAGED_WORKFLOW_IDS = [
      "payout-initiated",
      "weekly-digest",
      "order-shipped",
    ] as const;

    /** Build the definition for one workflowId from your in-repo registry. */
    function buildWorkflowDefinition(workflowId: string) {
      // Example: return workflowDefinitions[workflowId];
      return {
        name: "Payout Initiated",
        description: "This workflow is used to initiate a payout",
        tags: ["payout"],
        preferences: { /* ... */ },
        origin: "external" as const,
        validatePayload: false,
        payloadSchema: { /* ... */ },
        steps: [ /* ... */ ],
      };
    }

    export async function createOrUpdateWorkflowInDevelopment(workflowId: string) {
      const definition = buildWorkflowDefinition(workflowId);

      try {
        return await novu.workflows.update(definition, workflowId);
      } catch (error: unknown) {
        const statusCode =
          error && typeof error === "object" && "statusCode" in error
            ? (error as { statusCode: number }).statusCode
            : undefined;

        if (statusCode === 404) {
          return await novu.workflows.create({
            ...definition,
            workflowId,
          });
        }

        throw error;
      }
    }

    export async function createOrUpdateAllWorkflowsInDevelopment() {
      for (const workflowId of MANAGED_WORKFLOW_IDS) {
        await createOrUpdateWorkflowInDevelopment(workflowId);
      }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import os

    import novu_py
    from novu_py import Novu, models

    MANAGED_WORKFLOW_IDS = (
        "payout-initiated",
        "weekly-digest",
        "order-shipped",
    )


    def build_workflow_definition(workflow_id: str) -> novu_py.UpdateWorkflowDto:
        # Example: return WORKFLOW_REGISTRY[workflow_id]
        return novu_py.UpdateWorkflowDto(
            name="Payout Initiated",
            description="This workflow is used to initiate a payout",
            tags=["payout"],
            preferences=...,  # type: ignore
            origin=novu_py.ResourceOriginEnum.EXTERNAL,
            validate_payload=False,
            payload_schema={...},
            steps=[...],
        )


    def create_or_update_workflow_in_development(workflow_id: str) -> object:
        with Novu(secret_key=os.environ["NOVU_SECRET_KEY"]) as novu:
            definition = build_workflow_definition(workflow_id)

            try:
                return novu.workflows.update(
                    workflow_id=workflow_id,
                    update_workflow_dto=definition,
                )
            except models.NovuError as error:
                if error.status_code == 404:
                    return novu.workflows.create(
                        create_workflow_dto=novu_py.CreateWorkflowDto(
                            workflow_id=workflow_id,
                            name=definition.name,
                            description=definition.description,
                            tags=definition.tags,
                            validate_payload=definition.validate_payload,
                            payload_schema=definition.payload_schema,
                            steps=definition.steps,
                            preferences=definition.preferences,
                        )
                    )

                raise


    def create_or_update_all_workflows_in_development() -> None:
        for workflow_id in MANAGED_WORKFLOW_IDS:
            create_or_update_workflow_in_development(workflow_id)
    ```
  </Tab>
</Tabs>

For create and update field definitions, refer to [Create a workflow](/api-reference/workflows/create-a-workflow) and [Update a workflow](/api-reference/workflows/update-a-workflow).

## Publish workflows with CI/CD

This section implements *phase 2*: publishing workflows from Development to other environments in CI/CD.

After phase 1 has run for every managed `workflowId` and Development matches your repository, call [Sync a workflow](/api-reference/workflows/sync-a-workflow) from CI/CD once per ID. Use your Development secret key and the target environment identifier from the [Novu Dashboard](https://dashboard.novu.co). Each workflow is copied under the same `workflowId` in the target environment.

<Callout type="info">
  Run sync after workflow definition changes land in your repository (for example, on merge to `main`). Production and other live environments are updated by syncing from Development, not by editing workflows directly in those environments.
</Callout>

Store these in your CI provider as secrets: `NOVU_SECRET_KEY` (Development) and `NOVU_TARGET_ENVIRONMENT_ID` (from the Novu Dashboard).

<Tabs items={['CI/CD (GitHub Actions)', 'Node.js', 'Python']}>
  <Tab value="CI/CD (GitHub Actions)">
    This job calls [Sync a workflow](/api-reference/workflows/sync-a-workflow) (`PUT /v2/workflows/{workflowId}/sync`) once per `workflowId` after phase 1 has updated Development.

    ```yaml
    name: Publish Novu workflows

    on:
      push:
        branches:
          - main

    jobs:
      sync-workflows:
        runs-on: ubuntu-latest
        steps:
          - name: Sync workflows to target environment
            env:
              # Development secret key — workflows are copied FROM Development
              NOVU_SECRET_KEY: ${{ secrets.NOVU_SECRET_KEY }}
              NOVU_TARGET_ENVIRONMENT_ID: ${{ secrets.NOVU_TARGET_ENVIRONMENT_ID }}
            run: |
              MANAGED_WORKFLOW_IDS=(
                payout-initiated
                weekly-digest
                order-shipped
              )

              for WORKFLOW_ID in "${MANAGED_WORKFLOW_IDS[@]}"; do
                echo "Syncing ${WORKFLOW_ID}..."
                curl -sf -X PUT "https://api.novu.co/v2/workflows/${WORKFLOW_ID}/sync" \
                  -H "Authorization: ApiKey ${NOVU_SECRET_KEY}" \
                  -H "Content-Type: application/json" \
                  -d "{\"targetEnvironmentId\": \"${NOVU_TARGET_ENVIRONMENT_ID}\"}"
              done
    ```
  </Tab>

  <Tab value="Node.js">
    ```typescript
    import { Novu } from "@novu/api";

    const novu = new Novu({
      secretKey: process.env.NOVU_SECRET_KEY!,
    });

    const MANAGED_WORKFLOW_IDS = [
      "payout-initiated",
      "weekly-digest",
      "order-shipped",
    ];

    async function publishAllWorkflows() {
      for (const workflowId of MANAGED_WORKFLOW_IDS) {
        await novu.workflows.sync(
          { targetEnvironmentId: process.env.NOVU_TARGET_ENVIRONMENT_ID! },
          workflowId
        );
      }
    }

    await publishAllWorkflows();
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import os

    from novu_py import Novu

    MANAGED_WORKFLOW_IDS = (
        "payout-initiated",
        "weekly-digest",
        "order-shipped",
    )

    with Novu(secret_key=os.environ["NOVU_SECRET_KEY"]) as novu:
        for workflow_id in MANAGED_WORKFLOW_IDS:
            novu.workflows.sync(
                workflow_id=workflow_id,
                sync_workflow_dto={
                    "target_environment_id": os.environ["NOVU_TARGET_ENVIRONMENT_ID"],
                },
            )
    ```
  </Tab>
</Tabs>

For sync request details, refer to [Sync a workflow](/api-reference/workflows/sync-a-workflow).

## Next steps

For individual workflow operations, refer to these guides:

<Cards cols={2}>
  <Card title="Create a workflow" icon={<PlusCircle />} href="/api-reference/workflows/create-a-workflow">
    Create a workflow with the Novu API.
  </Card>

  <Card title="Update a workflow" icon={<Pencil />} href="/api-reference/workflows/update-a-workflow">
    Update a workflow with the Novu API.
  </Card>

  <Card title="Delete a workflow" icon={<Trash2 />} href="/api-reference/workflows/delete-a-workflow">
    Delete a workflow with the Novu API.
  </Card>

  <Card title="Retrieve a workflow" icon={<FileSearch />} href="/api-reference/workflows/retrieve-a-workflow">
    Retrieve a workflow with the Novu API.
  </Card>

  <Card title="Sync a workflow" icon={<ArrowLeftRight />} href="/api-reference/workflows/sync-a-workflow">
    Sync a workflow with the Novu API.
  </Card>
</Cards>
