cjmellor/approval/approval-development

Claude Code and agent skill by cjmellor/approval

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Build and work with cjmellor/approval features, including approval workflows, custom states, time-based expirations, rollbacks, and event handling.

Install

npx vskill@latest install cjmellor/approval/approval-development

SKILL.md

When to use this skill

Use this skill when working with model approval workflows, intercepting create/update operations for review, managing approval states, setting up time-based expirations, or handling approval events using cjmellor/approval.

Core Concepts

  • Models use `MustBeApproved` trait — Intercepts creating and updating events, storing dirty data in the approvals table instead of persisting directly.
  • Approvals have states — Pending, Approved, Rejected (via ApprovalStatus enum), plus custom states defined in config.
  • Approve to persist — Data only reaches the model's table when explicitly approved.
  • Bypass with `withoutApproval()` — Skip the approval workflow for admin actions, seeders, etc.
  • Expiration support — Approvals can auto-reject, auto-postpone, or trigger custom logic after a deadline.

Setup

Add the MustBeApproved trait to any model that needs approval:

php
use Cjmellor\Approval\Concerns\MustBeApproved;
class Post extends Model
{
use MustBeApproved;
// Optional: only these attributes require approval, others persist immediately
protected array $approvalAttributes = ['title', 'content'];
}

Approving, Rejecting, and Postponing

php
use Cjmellor\Approval\Models\Approval;
// Query by state
$pending = Approval::pending()->get();
$approved = Approval::approved()->get();
$rejected = Approval::rejected()->get();
// Change state
Approval::where('id', 1)->approve(); // Persists new_data to the model's table
Approval::where('id', 2)->reject(); // Marks as rejected
Approval::where('id', 3)->postpone(); // Resets to pending
// Approve without persisting data (state change only)
Approval::where('id', 1)->approve(persist: false);
// Conditional helpers
$approval->approveIf($user->isAdmin());
$approval->rejectUnless($hasPermission);
$approval->postponeIf($needsMoreInfo);

Custom Approval States

Define custom states in config/approval.php:

php
'states' => [
'pending' => ['name' => 'Pending', 'default' => true],
'approved' => ['name' => 'Approved'],
'rejected' => ['name' => 'Rejected'],
'in_review' => ['name' => 'In Review'],
'needs_info' => ['name' => 'Needs Clarification'],
],
php
// Set any configured state
$approval->setState('in_review');
// Get current state (returns custom state if set, otherwise standard state)
$state = $approval->getState(); // 'in_review'
// Query by any state
Approval::whereState('in_review')->get();
Approval::whereState('pending')->get(); // Only genuinely pending, excludes custom states

Time-Based Expirations

php
// Set expiration with automatic action
$approval->expiresIn(hours: 48)->thenReject();
$approval->expiresIn(days: 7)->thenPostpone();
$approval->expiresIn(minutes: 30)->thenCustom(); // Handle via ApprovalExpired event listener
// Specific datetime
$approval->expiresIn(datetime: now()->addWeek());
// Check expiration
$approval->isExpired(); // bool
// Query by expiration
Approval::expired()->get();
Approval::notExpired()->get();
Approval::hasExpiration()->get();

Process expired approvals via the scheduler:

php
// In routes/console.php (Laravel 11+)
Schedule::command('approval:process-expired')->everyMinute();

Rollbacks

php
// Roll back an approved change (keeps Approved state by default)
$approval->rollback();
// Roll back and reset to Pending (requires re-approval)
$approval->rollback(bypass: false);
// Conditional rollback
$approval->rollback(condition: fn ($approval) => $approval->created_at->isToday());

Requestor / Creator Tracking

php
// Get who created the approval
$requestor = $approval->requestor; // The authenticated user at creation time
$creator = $approval->creator; // Same relationship, different name
// Filter by requestor
Approval::requestedBy($user)->get();
// Check if a specific user requested it
$approval->wasRequestedBy($user); // bool

Foreign Keys

php
// Default foreign key is 'user_id' — customize per model:
public function getApprovalForeignKeyName(): string
{
return 'author_id';
}
// Include the foreign key when creating:
Post::create(['title' => 'Hello', 'user_id' => auth()->id()]);

Events

All events extend ApprovalEvent with Approval $approval and ?Authenticatable $user:

EventWhen
ApprovalCreatedApproval request created
ModelApprovedApproval state set to approved
ModelRejectedApproval state set to rejected
ModelSetPendingApproval state set to pending (postponed)
ModelRolledBackApproved change rolled back
ApprovalExpiredExpired approval processed by scheduler
php
use Cjmellor\Approval\Events\ModelApproved;
class SendApprovalNotification
{
public function handle(ModelApproved $event): void
{
$approval = $event->approval;
$approver = $event->user;
// Send notification, log audit trail, etc.
}
}

Factory Support

Use the MustBeApprovedFactory trait in model factories to bypass approval in tests:

php
use Cjmellor\Approval\Concerns\MustBeApprovedFactory;
class PostFactory extends Factory
{
use MustBeApprovedFactory;
// ...
}
// In tests:
Post::factory()->withoutApproval()->create();

Bypassing Approval

php
// Bypass for a single operation
$post->withoutApproval()->update(['title' => 'Admin Override']);
// Check if bypass is active
$post->isApprovalBypassed(); // bool

How Data Flows

When a model with MustBeApproved is created or updated:

  1. The trait intercepts the creating/updating Eloquent event
  2. Dirty attributes are captured as new_data, original values as original_data
  3. If $approvalAttributes is defined, only those attributes go through approval — the rest persist immediately
  4. The foreign key (default user_id) is extracted and stored separately in the foreign_key column
  5. An Approval record is created with state = pending
  6. The original model operation is cancelled (returns false from the event)
  7. When approved, new_data is applied to the model via forceFill() + withoutApproval()->save()

The new_data and original_data columns are cast to AsArrayObject, so you can access them as arrays:

php
$approval->new_data['title']; // The proposed new title
$approval->original_data['title']; // The original title before the change

Common Patterns

Building an Approval Dashboard
php
// Get all pending approvals with their related models
$approvals = Approval::pending()
->with('approvalable', 'requestor')
->latest()
->paginate(20);
// In a Blade view
@foreach ($approvals as $approval)
<div>
<p>{{ $approval->requestor?->name }} wants to change {{ class_basename($approval->approvalable_type) }}</p>
<p>Changes: {{ json_encode($approval->new_data->toArray()) }}</p>
<form method="POST" action="/approvals/{{ $approval->id }}/approve">
@csrf
<button type="submit">Approve</button>
</form>
</div>
@endforeach
Approval Controller
php
class ApprovalController extends Controller
{
public function approve(Approval $approval)
{
$approval->approve();
return back()->with('success', 'Changes approved and applied.');
}
public function reject(Approval $approval)
{
$approval->reject();
return back()->with('success', 'Changes rejected.');
}
}
Duplicate Detection

The package prevents duplicate pending approvals for the same model with identical data. If a user submits the same change twice, only one approval record is created.

JSON / Array Cast Attributes

Models with json or array casts work correctly. The approval system decodes JSON values when storing new_data and re-casts them when approving. Nested arrays and complex data structures are preserved.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
370
Repository forks
23
7-day trend score
30

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/cjmellor/approval/approval-development/badge)](https://verified-skill.com/skills/cjmellor/approval/approval-development)

Something wrong with this skill or its page? Tell us.