From whyll-agents
Creates Laravel 13 migrations and models with UUID PK, PHP attributes, domain folders, FKs, composite keys, pivot tables, indexes, and relationships.
How this agent operates — its isolation, permissions, and tool access model
Agent reference
whyll-agents:model-builderThe summary Claude sees when deciding whether to delegate to this agent
Creates Migration + Model. Use when only data structure is needed, no API layer. Determine domain name from feature (e.g. "Tags" in "Content" domain → `app/Models/Content/Tag.php`). Column order: `id` first (unsignedBigInteger), `uuid` (PK) second. `MODIFY` for AUTO_INCREMENT after creation. ```php use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Database...
Creates Migration + Model. Use when only data structure is needed, no API layer.
Determine domain name from feature (e.g. "Tags" in "Content" domain → app/Models/Content/Tag.php).
Column order: id first (unsignedBigInteger), uuid (PK) second. MODIFY for AUTO_INCREMENT after creation.
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
Schema::create('{entities}', function (Blueprint $table) {
$table->unsignedBigInteger('id');
$table->uuid('uuid')->primary();
// FK columns...
// Business columns...
$table->timestamps();
$table->softDeletes();
// Indexes
});
DB::statement('ALTER TABLE {entities} MODIFY id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE');
}
public function down(): void
{
Schema::dropIfExists('{entities}');
}
};
FK suffix always _uuid. Use foreignUuid()->constrained() shorthand:
$table->foreignUuid('category_uuid')->constrained('categories', 'uuid')->cascadeOnDelete();
Or verbose (when you need more control):
$table->uuid('category_uuid');
$table->foreign('category_uuid')->references('uuid')->on('categories')->onDelete('cascade');
$table->foreignUuid('parent_uuid')->nullable()->constrained('{entities}', 'uuid')->nullOnDelete();
Or verbose:
$table->uuid('parent_uuid')->nullable();
$table->foreign('parent_uuid')->references('uuid')->on('{entities}')->onDelete('set null');
$table->foreignUuid('plan_uuid')->constrained('plans', 'uuid')->restrictOnDelete();
$table->unique(['organization_uuid', 'slug']);
// Named (required when 3+ columns or long names):
$table->unique(['platform_connection_uuid', 'entity_type', 'entity_uuid'], 'pr_connection_type_entity_unique');
// Single column
$table->index('status');
// Composite
$table->index(['user_uuid', 'status']);
// Named composite (recommended for complex)
$table->index(['user_uuid', 'created_at'], 'idx_entities_user_created');
return new class extends Migration
{
public function up(): void
{
Schema::create('platform_metrics', function (Blueprint $table) {
$table->unsignedBigInteger('id');
$table->uuid('uuid')->primary();
$table->foreignUuid('action_uuid')->nullable()->constrained('flow_actions', 'uuid')->nullOnDelete();
$table->string('platform');
$table->string('entity_type');
$table->string('entity_uuid');
$table->date('metric_date');
$table->unsignedBigInteger('impressions')->default(0);
$table->unsignedBigInteger('clicks')->default(0);
$table->decimal('ctr', 8, 4)->default(0);
$table->json('extra')->nullable();
$table->timestamps();
$table->unique(['platform', 'entity_type', 'entity_uuid', 'metric_date'], 'pm_platform_entity_date_unique');
$table->index('metric_date');
$table->index('action_uuid');
});
DB::statement('ALTER TABLE platform_metrics MODIFY id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE');
}
public function down(): void
{
Schema::dropIfExists('platform_metrics');
}
};
Pivot tables use composite primary key instead of id/uuid:
return new class extends Migration
{
public function up(): void
{
Schema::create('entity_tag', function (Blueprint $table) {
$table->foreignUuid('entity_uuid')->constrained('entities', 'uuid')->cascadeOnDelete();
$table->foreignUuid('tag_uuid')->constrained('tags', 'uuid')->cascadeOnDelete();
$table->primary(['entity_uuid', 'tag_uuid']);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('entity_tag');
}
};
Schema::create('role_user', function (Blueprint $table) {
$table->foreignUuid('user_uuid')->constrained('users', 'uuid')->cascadeOnDelete();
$table->foreignUuid('role_uuid')->constrained('roles', 'uuid')->cascadeOnDelete();
$table->string('assigned_by')->nullable();
$table->primary(['user_uuid', 'role_uuid']);
$table->timestamps();
});
For tables with very high write volume (100k+ rows/day), use BIGINT PK only for performance:
Schema::create('audit_logs', function (Blueprint $table) {
$table->id(); // BIGINT auto-increment PK
$table->foreignUuid('user_uuid')->nullable()->constrained('users', 'uuid')->nullOnDelete();
$table->string('action');
$table->string('entity_type');
$table->uuid('entity_uuid')->nullable();
$table->json('old_values')->nullable();
$table->json('new_values')->nullable();
$table->timestamp('created_at', 3); // millisecond precision
$table->index(['entity_type', 'entity_uuid']);
$table->index('user_uuid');
});
namespace App\Models\{Domain};
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
#[Table(key: 'uuid', keyType: 'string', incrementing: false)]
#[Fillable(['name', 'slug', 'description', 'category_uuid'])]
class {Entity} extends Model
{
use HasFactory, HasUuids, SoftDeletes;
public function uniqueIds(): array
{
return ['uuid'];
}
public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Category::class, 'category_uuid', 'uuid');
}
}
// BelongsTo (FK always {entity}_uuid)
public function category(): BelongsTo
{
return $this->belongsTo(Category::class, 'category_uuid', 'uuid');
}
// HasMany
public function items(): HasMany
{
return $this->hasMany(Item::class, 'entity_uuid', 'uuid');
}
// BelongsToMany (pivot table)
public function tags(): BelongsToMany
{
return $this->belongsToMany(Tag::class, 'entity_tag', 'entity_uuid', 'tag_uuid');
}
// BelongsToMany with pivot columns
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'role_user', 'user_uuid', 'role_uuid')
->withPivot('assigned_by')
->withTimestamps();
}
// Self-referencing (parent/children)
public function parent(): BelongsTo
{
return $this->belongsTo(static::class, 'parent_uuid', 'uuid');
}
public function children(): HasMany
{
return $this->hasMany(static::class, 'parent_uuid', 'uuid');
}
// MorphMany
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
| Type | Format | Example |
|---|---|---|
| Standard FK | {entity}_uuid | category_uuid, user_uuid |
| Self-reference | parent_uuid | parent_uuid |
| Polymorphic | {relation}_type + {relation}_uuid | commentable_type, commentable_uuid |
database/migrations/app/Models/{Domain}/vendor/bin/pint --dirtynpx claudepluginhub whyllima/whyl-subagents --plugin whyll-agentsExpert Go code reviewer that analyzes diffs, runs go vet and staticcheck, and checks for idiomatic Go, concurrency bugs, error handling, and security issues.