Claude-skill-registry content-workflow
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
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/content-workflow" ~/.claude/skills/majiayu000-claude-skill-registry-content-workflow && rm -rf "$T"
manifest:
skills/data/content-workflow/SKILL.mdsource content
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
- Version control with workflowscontent-versioning
- Workflow-enabled content typescontent-type-modeling
- Workflow API endpointsheadless-api-design