Skip to content

Pipelines & Stages ​

Deal and Ticket models include full pipeline and stage management via the HasPipeline and HasPipelineScopes traits.

Reading Pipelines ​

php
use Rollogi\LaravelHubspot\Pipelines\Pipeline;
use Rollogi\LaravelHubspot\Crm\Deal;

// List all deal pipelines
$pipelines = Pipeline::deals()->get();

// List all ticket pipelines
$pipelines = Pipeline::tickets()->get();

// Via the model
$pipelines = Deal::pipelines()->get();

// Find by ID
$pipeline = Pipeline::deals()->find('default');

// Find by label
$pipeline = Pipeline::deals()->findByLabel('Sales Pipeline');

Pipeline CRUD ​

php
use Rollogi\LaravelHubspot\Pipelines\Pipeline;

$builder = Pipeline::deals();

// Create
$pipeline = $builder->create([
    'label' => 'Enterprise Pipeline',
    'displayOrder' => 1,
    'stages' => [
        ['label' => 'Discovery', 'displayOrder' => 0, 'metadata' => ['probability' => '0.1', 'isClosed' => 'false']],
        ['label' => 'Proposal', 'displayOrder' => 1, 'metadata' => ['probability' => '0.5', 'isClosed' => 'false']],
    ],
]);

// Update
$pipeline = $builder->update('pipeline-id', ['label' => 'Renamed Pipeline']);

// Delete
$builder->delete('pipeline-id');

Stage CRUD ​

php
use Rollogi\LaravelHubspot\Pipelines\Pipeline;

$builder = Pipeline::deals();

// Add a stage
$stage = $builder->addStage('pipeline-id', [
    'label' => 'Negotiation',
    'displayOrder' => 2,
    'metadata' => ['probability' => '0.7', 'isClosed' => 'false'],
]);

// Update a stage
$stage = $builder->updateStage('pipeline-id', 'stage-id', ['label' => 'Final Review']);

// Delete a stage
$builder->deleteStage('pipeline-id', 'stage-id');

Working with Stages ​

php
use Rollogi\LaravelHubspot\Pipelines\Pipeline;

$pipeline = Pipeline::deals()->find('default');

// Navigate stages
$first = $pipeline->firstStage();
$last = $pipeline->lastStage();
$next = $pipeline->nextStage($first);
$prev = $pipeline->previousStage($last);

// Find stages
$stage = $pipeline->stage('closedwon');          // By ID
$stage = $pipeline->stageByLabel('Closed Won');  // By label
$stage = $pipeline->resolveStage('Closed Won');  // By ID or label

// Filter stages
$openStages = $pipeline->openStages();
$closedStages = $pipeline->closedStages();

Stage Metadata ​

Each stage has metadata indicating its probability and closed/won/lost status:

php
$stage->isOpen();    // Not closed
$stage->isClosed();  // Closed (won or lost)
$stage->isWon();     // Closed with probability >= 1.0
$stage->isLost();    // Closed with probability <= 0.0

$stage->metadata->probability; // e.g. 0.5

Moving Between Stages ​

php
use Rollogi\LaravelHubspot\Crm\Deal;

$deal = Deal::findOrFail('123');

// Move to a specific stage (by ID, label, or PipelineStage instance)
$deal->moveTo('qualifiedtobuy');
$deal->moveTo('Qualified to Buy');
$deal->moveTo($stageInstance);

// Sequential navigation
$deal->advance();  // Move to next stage (no-op if at last stage)
$deal->regress();  // Move to previous stage (no-op if at first stage)
$deal->reopen();   // Move to first stage

// Deal-specific shortcuts
$deal->closeWon();   // Move to the won stage (probability = 1.0)
$deal->closeLost();  // Move to the lost stage (probability = 0.0)

moveTo() throws InvalidTransitionException if the stage is not found in the pipeline. Moving to the current stage is a no-op.

Inspecting Stage State ​

php
$deal->isOpen();                    // Current stage is open
$deal->isClosed();                  // Current stage is closed
$deal->isWon();                     // Current stage is won
$deal->isLost();                    // Current stage is lost
$deal->isAtStage('closedwon');      // At a specific stage
$deal->isPastStage('contractsent'); // Past a specific stage (by display order)
$deal->isBeforeStage('closedwon');  // Before a specific stage

Pipeline Query Scopes ​

The HasPipelineScopes trait adds query scopes for filtering by pipeline state:

php
use Rollogi\LaravelHubspot\Crm\Deal;

// Filter by pipeline
Deal::query()->inPipeline('default')->get();

// Filter by stage
Deal::query()->atStage('closedwon')->get();

// Filter by open/closed status (uses 'default' pipeline stages)
Deal::query()->open()->get();
Deal::query()->closed()->get();

// Filter by position relative to a stage
Deal::query()->pastStage('contractsent')->get();
Deal::query()->betweenStages('appointmentscheduled', 'contractsent')->get();

INFO

The open(), closed(), pastStage(), and betweenStages() scopes resolve stages from the default pipeline.

Transition Validation ​

You can enforce transition rules via configuration. Define rules under the pipelines config key, namespaced by object type and pipeline ID:

php
// config/hubspot.php
'pipelines' => [
    'cache' => '1 hour',

    // Transition rules for deals in the 'default' pipeline
    'deals' => [
        'default' => [
            'transitions' => [
                'direction' => 'forward', // Only allow forward stage transitions
                'requirements' => [
                    // Properties required before entering a stage
                    'closedwon' => ['amount', 'closedate'],
                ],
            ],
        ],
    ],
],

The built-in ConfigTransitionValidator supports:

  • direction -- Set to 'forward' to prevent backward transitions. Default: 'any'.
  • requirements -- Map of stage IDs to required property names. The transition is rejected if any listed property is empty.

For custom validation logic, implement the TransitionValidator interface:

php
use Rollogi\LaravelHubspot\Api\Model;
use Rollogi\LaravelHubspot\Pipelines\PipelineStage;
use Rollogi\LaravelHubspot\Pipelines\TransitionValidator;

class MyValidator implements TransitionValidator
{
    public function validate(Model $model, PipelineStage $from, PipelineStage $to): bool
    {
        // Your validation logic
        return true;
    }

    public function message(Model $model, PipelineStage $from, PipelineStage $to): string
    {
        return 'Custom validation failed.';
    }
}

// config/hubspot.php
'pipelines' => [
    'deals' => [
        'default' => [
            'validator' => MyValidator::class,
        ],
    ],
],

Invalid transitions throw InvalidTransitionException with from, to, and reason properties.

Pipeline Events ​

Stage transitions dispatch events that follow the same before/after pattern as CRUD events:

EventFired WhenCancellable
StageChangingBefore a stage transitionYes
StageChangedAfter a stage transitionNo

Both events carry model, from, to, and pipeline properties.

php
use Illuminate\Support\Facades\Event;
use Rollogi\LaravelHubspot\Events\StageChanging;
use Rollogi\LaravelHubspot\Events\StageChanged;

// Cancel a transition by returning false
Event::listen(StageChanging::class, function (StageChanging $event): bool {
    // Prevent closing deals under $10k
    if ($event->to->isWon() && ($event->model->amount ?? 0) < 10000) {
        return false;
    }
    return true;
});

// React to completed transitions
Event::listen(StageChanged::class, function (StageChanged $event): void {
    logger()->info(sprintf(
        'Deal %s moved from %s to %s in pipeline %s',
        $event->model->id,
        $event->from->label,
        $event->to->label,
        $event->pipeline->label,
    ));
});

Pipeline Caching ​

Pipeline data is cached by default for 1 hour. Configure via hubspot.pipelines.cache:

php
// config/hubspot.php
'pipelines' => [
    'cache' => '1 hour',   // String interval (parsed via CarbonInterval)
    // 'cache' => 3600,    // Integer seconds
    // 'cache' => false,   // Disable caching
],

Cache is automatically invalidated when pipelines or stages are created, updated, or deleted via the PipelineBuilder.