Pipelines & Stages
Deal and Ticket models include full pipeline and stage management via the HasPipeline and HasPipelineScopes traits.
Reading Pipelines
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
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
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
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:
$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.5Moving Between Stages
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
$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 stagePipeline Query Scopes
The HasPipelineScopes trait adds query scopes for filtering by pipeline state:
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:
// 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:
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:
| Event | Fired When | Cancellable |
|---|---|---|
StageChanging | Before a stage transition | Yes |
StageChanged | After a stage transition | No |
Both events carry model, from, to, and pipeline properties.
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:
// 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.