Skip to content

Recipes

Common patterns and ready-to-use code examples for Stickle. Copy, paste, and adapt these recipes to your application.

Track Monthly Recurring Revenue (MRR)

Track MRR as a calculated attribute that updates automatically:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use StickleApp\Core\Traits\StickleEntity;

class Account extends Model
{
    use StickleEntity;

    public static function stickleTrackedAttributes(): array
    {
        return [
            'mrr',
            'plan_name',
            'subscription_count',
        ];
    }

    protected function mrr(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->subscriptions()
                ->where('status', 'active')
                ->sum('monthly_amount')
        );
    }

    protected function subscriptionCount(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->subscriptions()
                ->where('status', 'active')
                ->count()
        );
    }
}

Create a high-MRR segment:

php
<?php

namespace App\Segments;

use App\Models\Account;
use Illuminate\Database\Eloquent\Builder;
use StickleApp\Core\Contracts\Segment;
use StickleApp\Core\Filters\Filter;

class HighMRRAccounts extends Segment
{
    public string $model = Account::class;

    public function toBuilder(): Builder
    {
        return $this->model::query()
            ->stickleWhere(
                Filter::number('mrr')->greaterThan(5000)
            );
    }
}

Identify Churning Customers

Find customers who are showing signs of churn:

php
<?php

namespace App\Segments;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use StickleApp\Core\Attributes\StickleSegmentMetadata;
use StickleApp\Core\Contracts\Segment;
use StickleApp\Core\Filters\Filter;

#[StickleSegmentMetadata([
    'name' => 'At Risk of Churning',
    'description' => 'Paying customers with declining engagement',
    'exportInterval' => 60, // Check hourly
])]
class AtRiskCustomers extends Segment
{
    public string $model = User::class;

    public function toBuilder(): Builder
    {
        return $this->model::query()
            // Active subscription
            ->where('subscription_status', 'active')
            // Low recent activity
            ->stickleWhere(
                Filter::sessionCount()
                    ->count()
                    ->betweenDates(
                        startDate: now()->subDays(30),
                        endDate: now()
                    )
                    ->lessThan(3)
            )
            // Declining page views
            ->stickleWhere(
                Filter::eventCount('page_view')
                    ->count()
                    ->decreased()
                    ->betweenDateRanges(
                        compareToDateRange: [now()->subDays(60), now()->subDays(30)],
                        currentDateRange: [now()->subDays(30), now()]
                    )
            );
    }
}

Send re-engagement email when they enter this segment:

php
<?php

namespace App\Listeners;

use StickleApp\Core\Events\ObjectEnteredSegment;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
use App\Mail\ReEngagementOffer;

class SendReEngagementEmail implements ShouldQueue
{
    public function handle(ObjectEnteredSegment $event): void
    {
        if ($event->segment->as_class === 'AtRiskCustomers') {
            $user = $event->object;

            Mail::to($user)->send(new ReEngagementOffer($user));
        }
    }
}

Find Power Users

Identify your most engaged customers:

php
<?php

namespace App\Segments;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use StickleApp\Core\Contracts\Segment;
use StickleApp\Core\Filters\Filter;

class PowerUsers extends Segment
{
    public string $model = User::class;

    public function toBuilder(): Builder
    {
        return $this->model::query()
            // High session count
            ->stickleWhere(
                Filter::sessionCount()
                    ->count()
                    ->betweenDates(
                        startDate: now()->subDays(30),
                        endDate: now()
                    )
                    ->greaterThan(20)
            )
            // High feature usage
            ->stickleWhere(
                Filter::eventCount('feature:used')
                    ->count()
                    ->betweenDates(
                        startDate: now()->subDays(30),
                        endDate: now()
                    )
                    ->greaterThan(50)
            )
            // Growing engagement
            ->stickleWhere(
                Filter::eventCount('page_view')
                    ->count()
                    ->increased()
                    ->betweenDateRanges(
                        compareToDateRange: [now()->subDays(60), now()->subDays(30)],
                        currentDateRange: [now()->subDays(30), now()]
                    )
            );
    }
}

Send Email When User Enters Segment

Automatically notify users when they achieve a milestone:

php
<?php

namespace App\Listeners;

use StickleApp\Core\Events\ObjectEnteredSegment;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
use App\Mail\HighValueWelcome;
use App\Mail\PowerUserBadge;

class SegmentEntryNotification implements ShouldQueue
{
    public function handle(ObjectEnteredSegment $event): void
    {
        $user = $event->object;
        $segment = $event->segment->as_class;

        match ($segment) {
            'HighValueCustomers' => Mail::to($user)->send(new HighValueWelcome()),
            'PowerUsers' => Mail::to($user)->send(new PowerUserBadge()),
            default => null,
        };
    }
}

Track Feature Adoption

Monitor which features users are adopting:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use StickleApp\Core\Traits\StickleEntity;

class User extends Model
{
    use StickleEntity;

    public static function stickleTrackedAttributes(): array
    {
        return [
            'features_used_count',
            'core_features_adopted',
            'advanced_features_adopted',
        ];
    }

    protected function featuresUsedCount(): Attribute
    {
        return Attribute::make(
            get: fn () => $this->featureUsage()
                ->where('last_used_at', '>=', now()->subDays(30))
                ->distinct('feature_name')
                ->count()
        );
    }

    protected function coreFeaturesAdopted(): Attribute
    {
        $coreFeatures = ['dashboard', 'reports', 'export'];

        return Attribute::make(
            get: fn () => $this->featureUsage()
                ->whereIn('feature_name', $coreFeatures)
                ->where('last_used_at', '>=', now()->subDays(30))
                ->distinct('feature_name')
                ->count()
        );
    }

    protected function advancedFeaturesAdopted(): Attribute
    {
        $advancedFeatures = ['api', 'webhooks', 'integrations'];

        return Attribute::make(
            get: fn () => $this->featureUsage()
                ->whereIn('feature_name', $advancedFeatures)
                ->where('last_used_at', '>=', now()->subDays(30))
                ->distinct('feature_name')
                ->count()
        );
    }
}

Track feature usage from JavaScript:

javascript
// When user uses a feature
stickle.track('feature:used', {
    feature_name: 'export',
    format: 'csv',
    records: 1500
});

Calculate Customer Health Score

Build a comprehensive health score:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use StickleApp\Core\Traits\StickleEntity;

class Customer extends Model
{
    use StickleEntity;

    public static function stickleObservedAttributes(): array
    {
        return [
            'health_score', // Alert when score changes
        ];
    }

    public static function stickleTrackedAttributes(): array
    {
        return [
            'health_score',
            'engagement_score',
            'payment_score',
            'support_score',
        ];
    }

    protected function healthScore(): Attribute
    {
        return Attribute::make(
            get: function () {
                $score = 0;

                // Engagement (40 points max)
                $score += $this->engagementScore * 0.4;

                // Payment health (30 points max)
                $score += $this->paymentScore * 0.3;

                // Support interactions (20 points max)
                $score += $this->supportScore * 0.2;

                // Feature adoption (10 points max)
                $featuresUsed = $this->features_used_count ?? 0;
                $score += min(10, $featuresUsed * 2);

                return round($score);
            }
        );
    }

    protected function engagementScore(): Attribute
    {
        return Attribute::make(
            get: function () {
                $score = 100;

                $daysSinceLogin = $this->last_login_at?->diffInDays(now()) ?? 999;

                if ($daysSinceLogin > 30) $score -= 60;
                elseif ($daysSinceLogin > 14) $score -= 40;
                elseif ($daysSinceLogin > 7) $score -= 20;

                $monthlyActiveDays = $this->monthly_active_days ?? 0;
                if ($monthlyActiveDays < 5) $score -= 20;

                return max(0, $score);
            }
        );
    }

    protected function paymentScore(): Attribute
    {
        return Attribute::make(
            get: function () {
                $score = 100;

                if ($this->subscription_status === 'past_due') $score -= 50;
                if ($this->subscription_status === 'canceled') return 0;
                if ($this->failed_payments_count > 0) $score -= 20;

                return max(0, $score);
            }
        );
    }

    protected function supportScore(): Attribute
    {
        return Attribute::make(
            get: function () {
                $score = 100;

                $openTickets = $this->supportTickets()
                    ->where('status', 'open')
                    ->count();

                $score -= ($openTickets * 10);

                return max(0, $score);
            }
        );
    }
}

Alert team when health score drops:

php
<?php

namespace App\Listeners;

use StickleApp\Core\Events\ObjectAttributeChanged;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Notification;
use App\Notifications\LowHealthScoreAlert;

class CustomerHealthScoreListener implements ShouldQueue
{
    public function handle(ObjectAttributeChanged $event): void
    {
        $customer = $event->object;
        $newScore = $event->newValue;
        $oldScore = $event->oldValue;

        // Alert if score drops below 50
        if ($newScore < 50 && $oldScore >= 50) {
            Notification::route('slack', config('slack.cs_webhook'))
                ->notify(new LowHealthScoreAlert($customer, $newScore));
        }

        // Escalate if critical
        if ($newScore < 25) {
            // Notify account manager directly
            $customer->accountManager->notify(new LowHealthScoreAlert($customer, $newScore));
        }
    }
}

Slack Notification on High-Value Login

Alert your team when VIP customers log in:

php
<?php

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Notification;
use App\Notifications\VIPLogin;
use StickleApp\Core\Filters\Filter;

class NotifyVIPLogin implements ShouldQueue
{
    public function handle(Login $event): void
    {
        $user = $event->user;

        // Check if high-value customer
        $isHighValue = $user->newQuery()
            ->where('id', $user->id)
            ->stickleWhere(
                Filter::segment('HighValueCustomers')->isInSegment()
            )
            ->exists();

        if ($isHighValue) {
            Notification::route('slack', config('slack.vip_webhook'))
                ->notify(new VIPLogin($user));
        }
    }
}

Update CRM When MRR Changes

Sync customer data to your CRM:

php
<?php

namespace App\Listeners;

use StickleApp\Core\Events\ObjectAttributeChanged;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Services\CRMService;

class AccountMrrListener implements ShouldQueue
{
    public function __construct(
        protected CRMService $crm
    ) {}

    public function handle(ObjectAttributeChanged $event): void
    {
        $account = $event->object;
        $newMRR = $event->newValue;
        $oldMRR = $event->oldValue;

        // Update CRM
        $this->crm->updateAccount($account->id, [
            'mrr' => $newMRR,
            'mrr_change' => $newMRR - $oldMRR,
            'updated_at' => now(),
        ]);

        // Tag high-value accounts
        if ($newMRR >= 5000 && $oldMRR < 5000) {
            $this->crm->addTag($account->id, 'high-value');
        }
    }
}

Trial Conversion Tracking

Track trial users and conversion:

php
<?php

namespace App\Segments;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use StickleApp\Core\Contracts\Segment;
use StickleApp\Core\Filters\Filter;

class ActiveTrialUsers extends Segment
{
    public string $model = User::class;

    public function toBuilder(): Builder
    {
        return $this->model::query()
            ->where('subscription_status', 'trial')
            ->where('trial_ends_at', '>', now())
            ->stickleWhere(
                Filter::sessionCount()
                    ->count()
                    ->betweenDates(now()->subDays(7), now())
                    ->greaterThan(3)
            );
    }
}

class TrialExpiringS Soon extends Segment
{
    public string $model = User::class;

    public function toBuilder(): Builder
    {
        return $this->model::query()
            ->where('subscription_status', 'trial')
            ->whereBetween('trial_ends_at', [now(), now()->addDays(3)]);
    }
}

Send conversion email before trial expires:

php
<?php

namespace App\Listeners;

use StickleApp\Core\Events\ObjectEnteredSegment;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
use App\Mail\TrialExpiringEmail;

class SendTrialExpiringEmail implements ShouldQueue
{
    public function handle(ObjectEnteredSegment $event): void
    {
        if ($event->segment->as_class === 'TrialExpiringSoon') {
            $user = $event->object;

            Mail::to($user)->send(new TrialExpiringEmail($user));
        }
    }
}

Next Steps