Reading time: 11 min read

Sitecore Workflow Automation with Management API

A practical guide to managing Sitecore workflows programmatically using the Management GraphQL API with production-tested patterns.

Portrait photo of Sohrab Saboori, article author

Why Automate Sitecore Workflows?

Automating Sitecore workflows is essential for building efficient content management systems. Whether you’re creating approval workflows, automating content publishing, or integrating with external systems, the Management API provides the tools you need. However, workflow operations require careful handling—you need admin-level permissions, proper error handling, and an understanding of workflow states and commands.

This guide focuses specifically on workflow automation using Sitecore’s Management API. We’ll cover how to query workflow information, discover available commands dynamically, and execute workflow operations programmatically. The patterns presented here are drawn from a production Next.js application that manages content approvals across multiple Sitecore sites, handling hundreds of workflow operations daily.

Prerequisites

Before working with workflow operations, ensure you have:

  1. Management API OAuth credentials configured with xmcloud.cm:admin scope
  2. Authentication service set up (see Connecting to Sitecore XM Cloud APIs)
  3. Items in workflow within your Sitecore instance
  4. Base client architecture implemented

Required Environment Variables:

# Management API (from authentication guide)SITECORE_MANAGEMENT_CLIENT_ID=your_management_client_id
SITECORE_MANAGEMENT_CLIENT_SECRET=your_management_client_secret
SITECORE_MANAGEMENT_ENDPOINT=https://xmc-[instance].sitecorecloud.io/sitecore/api/authoring/graphql/v1
SITECORE_MANAGEMENT_AUTH_ENDPOINT=https://auth.sitecorecloud.io/oauth/token
SITECORE_MANAGEMENT_AUDIENCE=https://api.sitecorecloud.io

Getting Started with Sitecore Authoring and Management GraphQL APIs

Understanding Sitecore Workflows

Workflow Structure

Workflow
  ├── States (Draft, Awaiting Approval, Approved, etc.)
  └── Commands (Submit, Approve, Reject, etc.)
       └── Available from specific states

Key Concepts

  • Workflow States: Represent stages in the content lifecycle (Draft, Awaiting Approval, Approved)
  • Workflow Commands: Actions that move items between states (Submit, Approve, Reject)
  • Command Availability: Commands are only available from specific states
  • Final States: States that represent the end of the workflow (usually “Approved” or “Published”)

API Requirements

Operation API Required Authentication
Read workflow info Either API API key or OAuth
Execute workflow commands Management API only OAuth with admin scope
Update item fields Authoring API OAuth

Workflow Operations Implementation

File Structure

src/lib/sitecore/
  ├── base-client.ts          # Base client (from auth guide)
  ├── workflow-operations.ts  # Workflow operations class
  └── index.ts               # Unified client

Query Workflow Information

File: src/lib/sitecore/workflow-operations.ts

Purpose: Retrieves current workflow state and available commands for an item. This is the foundation for all workflow operations—you must know the current state before executing any commands.

import { BaseSitecoreClient } from "./base-client";
import { sitecoreManagementAuthService } from "../sitecore-management-auth";
export class WorkflowOperations extends BaseSitecoreClient {
  /**
   * Get current workflow information for an item
   */
  async getCurrentWorkflowInfo(itemId: string) {
    const token = await sitecoreManagementAuthService.getAccessToken();
    const query = `
      query GetItemWorkflow($itemId: ID!) {
        item(where: { itemId: $itemId }) {
          itemId
          name
          workflow {
            workflow {
              workflowId
              displayName
            }
            workflowState {
              displayName
              stateId
            }
          }
        }
      }
    `;
    const response = await fetch(this.managementEndpoint!, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({
        query,
        variables: { itemId: this.formatGuid(itemId) },
      }),
    });
    if (!response.ok) {
      throw new Error(`Failed to get workflow info: ${response.status}`);
    }
    const result = await response.json();
    if (result.errors) {
      throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
    }
    return result.data;
  }
  /**
   * Get available commands from current state
   */
  async getAvailableCommands(workflowId: string, currentStateId: string) {
    const token = await sitecoreManagementAuthService.getAccessToken();
    const query = `
      query GetWorkflowCommands($workflowId: String!, $stateId: String!) {
        workflow(where: { workflowId: $workflowId }) {
          workflowId
          displayName
          commands(query: { stateId: $stateId }, first: 10) {
            edges {
              node {
                commandId
                displayName
              }
            }
          }
          states(first: 10) {
            edges {
              node {
                stateId
                displayName
                final
              }
            }
          }
        }
      }
    `;
    const response = await fetch(this.managementEndpoint!, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({
        query,
        variables: { workflowId, stateId: currentStateId },
      }),
    });
    if (!response.ok) {
      throw new Error(`Failed to get commands: ${response.status}`);
    }
    const result = await response.json();
    if (result.errors) {
      throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
    }
    return result.data;
  }
  private formatGuid(guid: string): string {
    const clean = guid.replace(/[-{}\s]/g, "").toUpperCase();
    const formatted = clean.replace(
      /^(.{8})(.{4})(.{4})(.{4})(.{12})$/,
      "$1-$2-$3-$4-$5"
    );
    return `{${formatted}}`;
  }
}

Response Example:

{
  "item": {
    "itemId": "{8F128029-CDC8-4C6D-B31D-9A9AE09A91E9}",
    "name": "Homepage",
    "workflow": {
      "workflow": {
        "workflowId": "{B8B71999-EB4D-42B6-971C-ED05D27255F9}",
        "displayName": "Sample Workflow"
      },
      "workflowState": {
        "displayName": "Awaiting Approval",
        "stateId": "{46DA5376-10DC-4B66-B464-AFDAA29DE06F}"
      }
    }
  }
}

Execute Workflow Commands

Purpose: Executes a specific workflow command to advance an item through the workflow. This is the core operation that changes workflow states.

export class WorkflowOperations extends BaseSitecoreClient {
  /**
   * Execute a workflow command
   */
  async executeWorkflowCommand(
    itemId: string,
    commandId: string,
    comments = "Automated via API"
  ) {
    const token = await sitecoreManagementAuthService.getAccessToken();
    const mutation = `
      mutation ExecuteWorkflowCommand(
        $item: ItemQueryInput!
        $commandId: String!
        $comments: String
      ) {
        executeWorkflowCommand(
          input: { 
            item: $item
            commandId: $commandId
            comments: $comments 
          }
        ) {
          successful
          completed
          message
          error
          nextStateId
        }
      }
    `;
    const response = await fetch(this.managementEndpoint!, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify({
        query: mutation,
        variables: {
          item: { itemId: this.formatGuid(itemId) },
          commandId,
          comments,
        },
      }),
    });
    if (!response.ok) {
      throw new Error(`Failed to execute command: ${response.status}`);
    }
    const result = await response.json();
    if (result.errors) {
      throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
    }
    return result.data.executeWorkflowCommand;
  }
}

Success Response:

{
  "successful": true,
  "completed": true,
  "message": "Item has been approved",
  "error": null,
  "nextStateId": "{07D8D14D-975F-43C5-ACFE-C8D0C70F5008}"
}

Error Response:

{
  "successful": false,
  "completed": false,
  "message": null,
  "error": "Command not available from current state",
  "nextStateId": null
}

Dynamic Workflow Advancement

Purpose: Automatically discovers and executes the appropriate workflow command. This advanced pattern eliminates the need to hardcode command IDs and makes your workflow automation more flexible and maintainable.

export class WorkflowOperations extends BaseSitecoreClient {
  /**
   * Automatically advance workflow to next state
   */
  async advanceWorkflow(itemId: string, preferredCommand?: string) {
    try {
      // Step 1: Get current workflow state
      const itemInfo = await this.getCurrentWorkflowInfo(itemId);
      if (!itemInfo.item?.workflow) {
        throw new Error("Item has no workflow or does not exist");
      }
      const workflow = itemInfo.item.workflow;
      const workflowId = workflow.workflow.workflowId;
      const currentStateId = workflow.workflowState.stateId;
      console.log("Current state:", workflow.workflowState.displayName);
      // Step 2: Get available commands
      const workflowData = await this.getAvailableCommands(
        workflowId,
        currentStateId
      );
      const availableCommands = workflowData.workflow.commands.edges;
      if (availableCommands.length === 0) {
        throw new Error("No commands available from current state");
      }
      // Step 3: Select appropriate command
      let selectedCommand = availableCommands[0].node;
      if (preferredCommand) {
        // Look for preferred command by name
        const found = availableCommands.find((cmd) =>
          cmd.node.displayName
            .toLowerCase()
            .includes(preferredCommand.toLowerCase())
        );
        if (found) {
          selectedCommand = found.node;
        }
      } else {
        // Auto-select "Approve" or "Submit" if available
        const autoCommand = availableCommands.find(
          (cmd) =>
            cmd.node.displayName.toLowerCase().includes("approve") ||
            cmd.node.displayName.toLowerCase().includes("submit")
        );
        if (autoCommand) {
          selectedCommand = autoCommand.node;
        }
      }
      console.log("Executing command:", selectedCommand.displayName);
      // Step 4: Execute the command
      const result = await this.executeWorkflowCommand(
        itemId,
        selectedCommand.commandId,
        `Automated via API: ${selectedCommand.displayName}`
      );
      if (result.successful) {
        return {
          success: true,
          currentState: workflow.workflowState.displayName,
          commandUsed: selectedCommand.displayName,
          message: result.message,
          nextStateId: result.nextStateId,
        };
      } else {
        throw new Error(result.error || "Workflow advancement failed");
      }
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : "Unknown error",
      };
    }
  }
}
export const workflowOperations = new WorkflowOperations();

Usage:

// Simple usage - auto-selects command
const result = await workflowOperations.advanceWorkflow(itemId);
if (result.success) {
  console.log(`Advanced from "${result.currentState}"`);
  console.log(`Used command: "${result.commandUsed}"`);
  console.log(`Message: ${result.message}`);
}
// With preferred command
const result = await workflowOperations.advanceWorkflow(itemId, "approve");

API Route Example

File: src/pages/api/workflow/advance.ts

Purpose: Provides an HTTP endpoint for workflow operations. This allows frontend applications to trigger workflow commands without handling OAuth tokens directly.

import { NextApiRequest, NextApiResponse } from "next";
import { workflowOperations } from "@/lib/sitecore/workflow-operations";
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method not allowed" });
  }
  try {
    const { itemId, commandId, preferredCommand } = req.body;
    if (!itemId) {
      return res.status(400).json({ error: "itemId is required" });
    }
    let result;
    if (commandId) {
      // Execute specific command
      result = await workflowOperations.executeWorkflowCommand(
        itemId,
        commandId
      );
    } else {
      // Use dynamic advancement
      result = await workflowOperations.advanceWorkflow(
        itemId,
        preferredCommand
      );
    }
    if (result.success || result.successful) {
      return res.status(200).json({ success: true, ...result });
    } else {
      return res.status(400).json({
        success: false,
        error: result.error || "Workflow operation failed",
      });
    }
  } catch (error) {
    console.error("Workflow API error:", error);
    return res.status(500).json({
      success: false,
      error: error instanceof Error ? error.message : "Unknown error",
    });
  }
}

Usage from frontend:

// Advance workflow automatically
const response = await fetch("/api/workflow/advance", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ itemId: "8F128029-CDC8-4C6D-B31D-9A9AE09A91E9" }),
});
const result = await response.json();

Best Practices

1. Always Check Current State First

/ ❌ WRONG - Execute command blindly
await workflowOperations.executeWorkflowCommand(itemId, commandId);
// ✅ CORRECT - Check state first
const info = await workflowOperations.getCurrentWorkflowInfo(itemId);
if (info.item?.workflow) {
  const commands = await workflowOperations.getAvailableCommands(
    info.item.workflow.workflow.workflowId,
    info.item.workflow.workflowState.stateId
  );
  // Then execute appropriate command
}

2. Use Dynamic Command Discovery

// ❌ WRONG - Hardcoded command IDs
const commandId = "{D3C3D04D-2C0D-4D5B-882E-08BABBA85E52}";
await workflowOperations.executeWorkflowCommand(itemId, commandId);
// ✅ CORRECT - Dynamic discovery
const result = await workflowOperations.advanceWorkflow(itemId, "approve");

3. Handle Workflow Errors Gracefully

const result = await workflowOperations.advanceWorkflow(itemId);
if (!result.success) {
  if (result.error?.includes("no workflow")) {
    console.error("Item is not in a workflow");
  } else if (result.error?.includes("No commands available")) {
    console.error("Item is in final state");
  } else {
    console.error("Workflow error:", result.error);
  }
}

Common Pitfalls

❌ Pitfall 1: Using Authoring API for Workflow Commands

Problem: Workflow commands require Management API with admin scope.

Solution: Always use Management API OAuth token for executeWorkflowCommand.

❌ Pitfall 2: Not Checking Item Workflow Status

Problem: Executing commands on items not in workflow causes errors.

Solution: Always call getCurrentWorkflowInfo() first to verify workflow exists.

❌ Pitfall 3: Hardcoding Command IDs

Problem: Command IDs differ across environments and workflows.

Solution: Use dynamic command discovery based on command names.

❌ Pitfall 4: Ignoring Final States

Problem: Trying to advance items already in final state.

Solution: Check if state.final === true before attempting advancement.

❌ Pitfall 5: Missing Error Context

Problem: Generic errors make debugging difficult.

Solution: Include itemId, current state, and command in error logs.


Testing Workflow Operations

File: src/pages/api/debug/test-workflow.ts

Purpose: Validates workflow operations are configured correctly. This test endpoint helps verify your Management API credentials and workflow setup before deploying to production.


import { NextApiRequest, NextApiResponse } from "next";
import { workflowOperations } from "@/lib/sitecore/workflow-operations";
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { itemId } = req.query;
  if (!itemId || typeof itemId !== "string") {
    return res.status(400).json({ error: "itemId query parameter required" });
  }
  try {
    // Test 1: Get workflow info
    console.log("🔍 Testing workflow info query...");
    const info = await workflowOperations.getCurrentWorkflowInfo(itemId);
    if (!info.item?.workflow) {
      return res.status(200).json({
        success: true,
        hasWorkflow: false,
        message: "Item exists but is not in a workflow",
      });
    }
    // Test 2: Get available commands
    console.log("🔍 Testing command discovery...");
    const commands = await workflowOperations.getAvailableCommands(
      info.item.workflow.workflow.workflowId,
      info.item.workflow.workflowState.stateId
    );
    res.status(200).json({
      success: true,
      hasWorkflow: true,
      currentState: info.item.workflow.workflowState.displayName,
      workflowName: info.item.workflow.workflow.displayName,
      availableCommands: commands.workflow.commands.edges.map(
        (cmd) => cmd.node.displayName
      ),
      allStates: commands.workflow.states.edges.map((state) => ({
        name: state.node.displayName,
        final: state.node.final,
      })),
    });
  } catch (error) {
    console.error("❌ Workflow test failed:", error);
    res.status(500).json({
      success: false,
      error: error instanceof Error ? error.message : "Unknown error",
    });
  }
}

Test it:

curl "http://localhost:3001/api/debug/test-workflow?itemId={YOUR-ITEM-GUID}"

Final Thoughts on Workflow Automation

Automating Sitecore workflows with the Management API transforms how you manage content approvals and publishing processes. The key to success is understanding that workflow operations are fundamentally different from content operations—they require admin-level permissions, careful state management, and dynamic command discovery rather than hardcoded IDs.

The patterns presented here—checking state before execution, discovering commands dynamically, and handling errors gracefully—have proven reliable in production environments processing hundreds of workflow operations daily. By implementing these approaches, you’ll build workflow automation that’s flexible enough to handle different workflows across environments while maintaining the robustness needed for production use. Remember to always verify the current workflow state before executing commands, use meaningful audit comments, and log operations for debugging and compliance purposes.