content-workflow

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

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "content-workflow" with this command: npx skills add melodic-software/claude-code-plugins/melodic-software-claude-code-plugins-content-workflow

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&#x3C;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 &#x26;&#x26; 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

design-thinking

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

plantuml-syntax

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

system-prompt-engineering

No summary provided by upstream source.

Repository SourceNeeds Review