Claude Code Plugins

Community-maintained marketplace

Feedback

Use when implementing editorial workflows, approval chains, scheduled publishing, or role-based content permissions. Covers workflow states, transitions, notifications, and workflow APIs for headless CMS.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name content-workflow
description Use when implementing editorial workflows, approval chains, scheduled publishing, or role-based content permissions. Covers workflow states, transitions, notifications, and workflow APIs for headless CMS.
allowed-tools Read, Glob, Grep, Task, Skill

Content Workflow

Guidance for implementing editorial workflows, approval processes, and scheduled publishing in headless CMS systems.

When to Use This Skill

  • Implementing multi-stage approval workflows
  • Adding scheduled/embargo publishing
  • Building content moderation systems
  • Defining role-based publishing permissions
  • Automating workflow notifications

Workflow States

Basic Workflow States

public enum WorkflowState
{
    Draft,          // Initial creation, author editing
    InReview,       // Submitted for review
    Approved,       // Approved, ready to publish
    Published,      // Live content
    Unpublished,    // Removed from live
    Archived,       // Long-term storage
    Rejected        // Review rejected, needs revision
}

Extended Workflow States

public enum ExtendedWorkflowState
{
    // Creation
    Draft,

    // Review stages
    PendingEditorialReview,
    EditorialApproved,
    PendingLegalReview,
    LegalApproved,
    PendingFinalApproval,

    // Publication
    Scheduled,
    Published,

    // Post-publication
    Unpublished,
    Expired,
    Archived
}

State Machine Implementation

Workflow Definition

public class WorkflowDefinition
{
    public string Name { get; set; } = string.Empty;
    public List<WorkflowStateDefinition> States { get; set; } = new();
    public List<WorkflowTransition> Transitions { get; set; } = new();
}

public class WorkflowStateDefinition
{
    public string Name { get; set; } = string.Empty;
    public bool IsInitial { get; set; }
    public bool IsFinal { get; set; }
    public List<string> AllowedRoles { get; set; } = new();
    public List<string> RequiredFields { get; set; } = new();
}

public class WorkflowTransition
{
    public string Name { get; set; } = string.Empty;
    public string FromState { get; set; } = string.Empty;
    public string ToState { get; set; } = string.Empty;
    public List<string> AllowedRoles { get; set; } = new();
    public bool RequiresComment { get; set; }
    public List<string> Notifications { get; set; } = new();
}

State Machine Service

public class WorkflowService
{
    public async Task<bool> CanTransitionAsync(
        ContentItem content,
        string transition,
        ClaimsPrincipal user)
    {
        var workflow = await GetWorkflowAsync(content.ContentType);
        var transitionDef = workflow.Transitions
            .FirstOrDefault(t => t.Name == transition
                && t.FromState == content.WorkflowState);

        if (transitionDef == null)
            return false;

        // Check role permissions
        var userRoles = user.Claims
            .Where(c => c.Type == ClaimTypes.Role)
            .Select(c => c.Value);

        return transitionDef.AllowedRoles
            .Any(r => userRoles.Contains(r));
    }

    public async Task TransitionAsync(
        Guid contentId,
        string transition,
        string? comment,
        ClaimsPrincipal user)
    {
        var content = await _repository.GetAsync(contentId);
        if (!await CanTransitionAsync(content!, transition, user))
            throw new WorkflowException("Transition not allowed");

        var workflow = await GetWorkflowAsync(content!.ContentType);
        var transitionDef = workflow.Transitions
            .First(t => t.Name == transition);

        // Validate required fields for target state
        var targetState = workflow.States
            .First(s => s.Name == transitionDef.ToState);
        await ValidateRequiredFieldsAsync(content, targetState);

        // Record transition
        var history = new WorkflowHistory
        {
            Id = Guid.NewGuid(),
            ContentItemId = contentId,
            FromState = content.WorkflowState,
            ToState = transitionDef.ToState,
            Transition = transition,
            Comment = comment,
            UserId = user.GetUserId(),
            OccurredUtc = DateTime.UtcNow
        };

        content.WorkflowState = transitionDef.ToState;
        content.ModifiedUtc = DateTime.UtcNow;

        await _repository.UpdateAsync(content);
        await _historyRepository.AddAsync(history);

        // Send notifications
        await SendNotificationsAsync(content, transitionDef);
    }
}

Scheduled Publishing

Schedule Model

public class PublishSchedule
{
    public Guid ContentItemId { get; set; }

    // Publishing window
    public DateTime? PublishAtUtc { get; set; }
    public DateTime? UnpublishAtUtc { get; set; }

    // Recurrence (optional)
    public bool IsRecurring { get; set; }
    public string? CronExpression { get; set; }

    // Status
    public ScheduleStatus Status { get; set; }
    public DateTime? LastExecutedUtc { get; set; }
}

public enum ScheduleStatus
{
    Pending,
    Published,
    Completed,
    Failed,
    Cancelled
}

Scheduled Publishing Service

public class ScheduledPublishingService
{
    public async Task SchedulePublishAsync(
        Guid contentId,
        DateTime publishAtUtc,
        DateTime? unpublishAtUtc = null)
    {
        var content = await _repository.GetAsync(contentId);
        if (content == null)
            throw new NotFoundException();

        // Validate content is ready to publish
        await ValidateForPublishAsync(content);

        var schedule = new PublishSchedule
        {
            ContentItemId = contentId,
            PublishAtUtc = publishAtUtc,
            UnpublishAtUtc = unpublishAtUtc,
            Status = ScheduleStatus.Pending
        };

        await _scheduleRepository.AddAsync(schedule);

        // Update content state
        content.WorkflowState = "Scheduled";
        content.ScheduledPublishUtc = publishAtUtc;
        await _repository.UpdateAsync(content);
    }

    // Called by background job
    public async Task ProcessScheduledItemsAsync()
    {
        var now = DateTime.UtcNow;

        // Items to publish
        var toPublish = await _scheduleRepository
            .GetPendingPublishAsync(now);

        foreach (var schedule in toPublish)
        {
            try
            {
                await PublishContentAsync(schedule.ContentItemId);
                schedule.Status = ScheduleStatus.Published;
                schedule.LastExecutedUtc = now;
            }
            catch (Exception ex)
            {
                schedule.Status = ScheduleStatus.Failed;
                _logger.LogError(ex, "Failed to publish {ContentId}",
                    schedule.ContentItemId);
            }

            await _scheduleRepository.UpdateAsync(schedule);
        }

        // Items to unpublish
        var toUnpublish = await _scheduleRepository
            .GetPendingUnpublishAsync(now);

        foreach (var schedule in toUnpublish)
        {
            await UnpublishContentAsync(schedule.ContentItemId);
            schedule.Status = ScheduleStatus.Completed;
            await _scheduleRepository.UpdateAsync(schedule);
        }
    }
}

Background Job Configuration

// Using Hangfire or similar
public class ScheduledPublishingJob
{
    private readonly ScheduledPublishingService _service;

    [AutomaticRetry(Attempts = 3)]
    public async Task Execute()
    {
        await _service.ProcessScheduledItemsAsync();
    }
}

// Registration
RecurringJob.AddOrUpdate<ScheduledPublishingJob>(
    "scheduled-publishing",
    job => job.Execute(),
    "*/5 * * * *"); // Every 5 minutes

Approval Chains

Multi-Stage Approval

public class ApprovalChain
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public List<ApprovalStage> Stages { get; set; } = new();
}

public class ApprovalStage
{
    public int Order { get; set; }
    public string Name { get; set; } = string.Empty;
    public ApprovalType Type { get; set; }
    public List<string> ApproverRoles { get; set; } = new();
    public List<Guid>? SpecificApproverIds { get; set; }
    public int RequiredApprovals { get; set; } = 1;
    public TimeSpan? Timeout { get; set; }
}

public enum ApprovalType
{
    AnyApprover,      // Any one from the list
    AllApprovers,     // Everyone must approve
    MajorityApprovers // Majority rule
}

Approval Tracking

public class ApprovalRequest
{
    public Guid Id { get; set; }
    public Guid ContentItemId { get; set; }
    public Guid ApprovalChainId { get; set; }
    public int CurrentStage { get; set; }
    public ApprovalStatus Status { get; set; }
    public DateTime RequestedUtc { get; set; }
    public DateTime? CompletedUtc { get; set; }

    public List<ApprovalDecision> Decisions { get; set; } = new();
}

public class ApprovalDecision
{
    public Guid Id { get; set; }
    public Guid ApprovalRequestId { get; set; }
    public int Stage { get; set; }
    public string ApproverId { get; set; } = string.Empty;
    public ApprovalDecisionType Decision { get; set; }
    public string? Comment { get; set; }
    public DateTime DecidedUtc { get; set; }
}

public enum ApprovalDecisionType
{
    Pending,
    Approved,
    Rejected,
    Delegated
}

Notifications

Notification Types

public enum WorkflowNotificationType
{
    // Review requests
    ReviewRequested,
    ApprovalRequired,

    // Decisions
    ContentApproved,
    ContentRejected,

    // Publishing
    ContentPublished,
    ContentScheduled,
    ContentExpiring,

    // Reminders
    ReviewOverdue,
    ApprovalOverdue
}

Notification Service

public class WorkflowNotificationService
{
    public async Task SendNotificationAsync(
        WorkflowNotificationType type,
        ContentItem content,
        IEnumerable<string> recipientIds,
        Dictionary<string, string>? extraData = null)
    {
        var template = await _templateService.GetAsync(type.ToString());

        foreach (var recipientId in recipientIds)
        {
            var recipient = await _userService.GetAsync(recipientId);

            var notification = new Notification
            {
                Id = Guid.NewGuid(),
                Type = type.ToString(),
                RecipientId = recipientId,
                Subject = template.RenderSubject(content, extraData),
                Body = template.RenderBody(content, extraData),
                ContentItemId = content.Id,
                CreatedUtc = DateTime.UtcNow
            };

            await _notificationRepository.AddAsync(notification);

            // Send via configured channels
            if (recipient.EmailNotifications)
                await _emailService.SendAsync(recipient.Email, notification);

            if (recipient.SlackNotifications)
                await _slackService.SendAsync(recipient.SlackId, notification);
        }
    }
}

Role-Based Permissions

Content Permissions

public class ContentPermission
{
    public string ContentType { get; set; } = string.Empty;
    public string Role { get; set; } = string.Empty;
    public ContentPermissionLevel Level { get; set; }
}

[Flags]
public enum ContentPermissionLevel
{
    None = 0,
    Read = 1,
    Create = 2,
    Edit = 4,
    Delete = 8,
    Publish = 16,
    Unpublish = 32,
    Archive = 64,
    ManageWorkflow = 128,
    Full = Read | Create | Edit | Delete | Publish | Unpublish | Archive | ManageWorkflow
}

Permission Checking

public class ContentAuthorizationService
{
    public async Task<bool> CanPerformActionAsync(
        ClaimsPrincipal user,
        ContentItem content,
        ContentPermissionLevel action)
    {
        var userRoles = user.Claims
            .Where(c => c.Type == ClaimTypes.Role)
            .Select(c => c.Value);

        var permissions = await _permissionRepository
            .GetByContentTypeAsync(content.ContentType);

        foreach (var role in userRoles)
        {
            var permission = permissions.FirstOrDefault(p => p.Role == role);
            if (permission != null && permission.Level.HasFlag(action))
                return true;
        }

        return false;
    }
}

API Design

Workflow Endpoints

GET    /api/content/{id}/workflow              # Get workflow state
POST   /api/content/{id}/workflow/transition   # Execute transition
GET    /api/content/{id}/workflow/history      # Transition history
GET    /api/content/{id}/workflow/available    # Available transitions

# Scheduling
POST   /api/content/{id}/schedule              # Schedule publish
DELETE /api/content/{id}/schedule              # Cancel schedule
GET    /api/scheduled                          # List scheduled items

# Approvals
POST   /api/content/{id}/submit-for-review     # Start approval
POST   /api/approvals/{id}/approve             # Approve
POST   /api/approvals/{id}/reject              # Reject
GET    /api/approvals/pending                  # My pending approvals

Related Skills

  • content-versioning - Version control with workflows
  • content-type-modeling - Workflow-enabled content types
  • headless-api-design - Workflow API endpoints