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.