Quote Follow-Up — Pipedrive pipelines & stages
Audience: developers & AI agents · Scope: the imported Pipedrive pipelines & stages that drive the landing page's project-stage picker — the entities, multi-pipeline handling, the picker, won-derivation, and the read-only admin lists · Last reviewed: 2026-07-24
A spoke of the Quote Follow-Up System. Consumed by landing-page.md (the picker + the save) and stage-sync.md (the sheet mirror).
TL;DR — Pipedrive pipelines and stages are imported into ConfigBox and curated in the backend. A quote follows its own pipeline (quotes.pipedrive_pipeline_id), and the landing page's stage buttons are the live, published stages of that pipeline — not a hard-coded list. Selecting a stage flagged is_won_stage marks the quote won (there is no "Won" button) and moves the real deal. Imported fields are read-only in the admin (a re-import is the only way to change them); admin-curated metadata (hint, visibility, nudge cadence) is preserved across re-imports.
The entities (Pipelines + Stages)
Both pipelines and stages are imported from Pipedrive and curated in the CB backend. Both the Imported Pipedrive Stages and Imported Pipedrive Pipelines lists are surfaced in Settings → Quote Follow-ups (that is where the follow-up cadence lives); the single Import pipelines & stages from Pipedrive task on the stages list populates both:
- Imported Pipedrive Stages —
#__configbox_external_pipedrive_stages. The one list task, Import pipelines & stages from Pipedrive, imports both in the right order: it first upserts the pipelines (importPipelines(),GET /pipelines) so the stages' pipeline references and the list's Pipeline filter resolve, then upserts all pipelines' stages (importStages(),GET /stageswith no pipeline filter). The stages list has a Pipeline dropdown filter ('filter'=>1onpipeline_id) to narrow to one pipeline, and default-sorts by the imported Pipedriveorder_nr. - Imported Pipedrive Pipelines —
#__configbox_external_pipedrive_pipelines, entityConfigboxModelAdminbcpipedrivepipelines(+ controller/views +adminBcPipedrivePipelinesJS). Populated by the same merged import above (importPipelines(), keyed onpipedrive_pipeline_id; a vanished pipeline is deactivated, never deleted). The standalone list + its own Import pipelines from Pipedrive button (and thecb_pipedrive_import_pipelinesCLI) still exist for direct access, but are not shown in Settings.
Files
All paths are under docroot/components/com_configbox/data/customization/.
| File | Role |
|---|---|
models/adminbcpipedrivepipelines.php + controllers/adminbcpipedrivepipelines.php + views/adminbcpipedrivepipeline(s)/ + assets/javascript/adminBcPipedrivePipelines.js | Backend list/form entity for the imported Pipedrive pipelines (importPipelines()). Not embedded in the settings form any more — populated by the stages list's merged import (below) and reachable directly if needed. |
models/adminbcpipedrivestages.php + controllers/adminbcpipedrivestages.php + views/adminbcpipedrivestage(s)/ | Backend list/form entity for the imported Pipedrive stages (all pipelines), embedded in the Quote Follow-ups settings group, with a stock Pipeline filter and the single Import pipelines & stages from Pipedrive list task — the controller runs importPipelines() then importStages() in one action. See pipedrive/admin-manual.md. |
properties/bcimported.php · bcimportedjoin.php · bcimportedboolean.php | The three read-only custom property types that render Pipedrive-owned fields as static text (below). |
properties/bcimportedchoice.php + system_overrides/BcPipedriveImportOption.php | The editable counterpart: the settings dropdown that picks a default from these imported rows, and the shared labelling/ordering of its options. See Default pipeline & stage. |
Read-only imports: the admin can't hand-edit Pipedrive-owned fields
Each edit form is split into two groups, each with an always-visible note:
- "Imported from Pipedrive (read-only)" — the Pipedrive-owned fields (id, name, pipeline, order,
probability, active flag). These are overwritten by every import, so the form renders them as static,
non-editable text. Three small custom property types under
data/customization/properties/do this by showing the value but never saving it:bcimported(plain values),bcimportedjoin(the pipeline — keeps the list's name + filter, but is static in the form), andbcimportedboolean(the active flag — Yes/No). Each extends its base property (KenedoProperty/…Join/…Boolean) andrequires the base file first, because property-type files load on demand. To change any of these, edit the stage/pipeline in Pipedrive and re-import. - "Follow-up settings (editable)" — the admin-curated fields below;
importStages()/importPipelines()preserve them on re-run.
The stages list shows the Pipedrive stage id right after the local ID column, under the shortened
heading "PD stage ID" (the form keeps "Pipedrive stage ID") — the two are easy to mix up, and it is
the Pipedrive id that quotes (quotes.pipedrive_stage_id), the nudge cadence and the deal push all key
on. The lists still hide the Order column (Order drives the default sort) and the stage Hint; the
visibility flag's list heading is shortened to "Visible" (the form keeps "Visible to agents"), and
the ID + name cells are edit links. There is no manual Add/Remove — a re-import is how rows appear or go
away.
The project stage picker (imported Pipedrive stages)
The stage buttons are the live stages of the quote's pipeline, not a hard-coded list.
ConfigboxModelBcquotelandingpage::getStagesForQuote($quote) returns the published stages of the
quote's pipeline (or the default pipeline when the quote has none), ordered by Pipedrive's order_nr.
getStages($pipelineId) is the pipeline-scoped primitive; getStageById($id) resolves any imported
stage across all pipelines (for the recap / sync).
Each stage row carries Pipedrive-owned fields (pipedrive_stage_id, pipeline_id, name, order_nr,
deal_probability, active_flag) refreshed by import (read-only in the admin — see above), plus
admin-curated metadata that Pipedrive does not provide and that import preserves:
hint— the caption under the stage button.is_won_stage— selecting this stage marks the quote won (there is no "Won" status button; won is derived from the stage — see Stage drives status below).published— whether the stage appears on the form (labelled "Visible to agents" in the admin).- the nudge cadence (
nudge_days_small/_mid/_large) — days a quote may sit in this stage, per quote-value bucket, before it is due for a follow-up nudge. See nudge-cadence.md. deal_probability— Pipedrive's win probability for the stage (0–100), Pipedrive-owned. Shown next to the stage title on the form (getStages()returns it; the picker rendersNN%).
A quote stores the chosen stage's numeric pipedrive_stage_id and its pipedrive_pipeline_id
(derived from the stage in saveExtraFeedback), so the stage still resolves if the deal is later moved
to another pipeline. Both are kept in sync with the deal in both directions: the outbound deal
update sends stage_id + pipeline_id (updatePipedriveDeal), and the inbound webhook maps
stage_id/pipeline_id and derives the pipeline from the incoming stage (buildInboundColumnChanges).
The picker n badge is a 1-based sequence over the published stages.
Stage drives status (there is no "Won" button). The status control offers only Open and Lost.
"Won" is derived purely from the chosen stage: selecting a stage flagged is_won_stage marks the quote
won server-side (saveAgentUpdate reads the stage's is_won). The status control stays on Open
while a won stage is selected (the stage's win probability signals that it secures the deal). This now
sets the real Pipedrive deal status — see Won really wins the deal.
Lost reasons (LOST_REASONS, matching the existing vocabulary): Price, Lead time,
Spec change, Lost to competitor, Project cancelled, Other. When status is Lost,
the stage and both dates (bid + completion) are cleared and a lost reason is required. Picking Other
reveals a required free-form multiline field; the category ("Other") is stored on
pipedrive_agent_feedback_lost_reason (→ the Agent Feedback Lost Reason custom field) and the typed
elaboration on the native pipedrive_lost_reason (→ the deal's native Lost reason). For any
non-Other reason both columns hold the category, as before. (The full field→Pipedrive mapping is in
landing-page.md → Data model.)
Won really wins the deal
Every submission sets the native Pipedrive deal status to match the landing-page outcome
(saveAgentUpdate → ConfigboxModelBcquotes::updatePipedriveDealStatus):
- a won-flagged stage → native status won,
- any non-won stage (status Open) → native status open — so a quote that was previously won is put back to open when a lower stage is chosen,
- Lost → native status lost + the (free-text-for-Other) native lost reason.
This is authoritative for the follow-up page and deliberately overrides the shared
updatePipedriveStatusConditionally() "agents only suggest" guard (which never propagates won and
won't move a deal off won). The My Quotes portal path is unchanged — it still only suggests.
pipedrive_agent_feedback_status continues to carry the agent's own read for the custom field.
Empty list tolerance: every stage consumer (getStages, validation, the template, the JS
gating) tolerates an empty list — before the first import the picker is omitted and an open/won
update is still submittable.
Default pipeline & stage
Stages are imported for all Pipedrive pipelines. A quote follows its own pipeline
(quotes.pipedrive_pipeline_id); a quote that has none yet falls back to two required settings that
say where a follow-up starts — both in Settings → Quote Follow-ups, both dropdowns over the imported
lists further down that same group:
| Setting | Reader | What it does |
|---|---|---|
Default pipeline (pipedrive_default_pipeline_id) | BcPipedriveConfig::defaultPipelineId() | The pipeline whose stages the picker shows for a quote with no pipeline of its own (getPipelineIdForQuote()). |
Default stage (pipedrive_default_stage_id) | BcPipedriveConfig::defaultStageId() | The stage pre-selected for a quote with no stage on file (getDefaultStageId($pipelineId)). |
Neither is written to the quote. A new quote keeps pipedrive_pipeline_id / pipedrive_stage_id
NULL until an agent submits the form (or the webhook writes a stage back) — the settings only decide
what the form starts on.
The stage falls back rather than lying. getDefaultStageId() honours the configured default stage
only when it is one of the stages on offer for the pipeline being rendered; otherwise it returns that
pipeline's lowest-order published stage (the pre-0.5.79 behaviour). That matters in two ordinary cases:
a quote on a different pipeline than the default one, and a default that went stale after a re-import.
The dropdowns list every imported row, retired/hidden ones suffixed "(retired in Pipedrive)" /
"(hidden from agents)" (BcPipedriveImportOption). And when the stored value matches no imported row at
all — a placeholder id, a row that predates the import, a leftover test stage — the property adds a selected
"(id N — not imported)" stand-in for it (KenedoPropertyBcimportedchoice::getParentModelRecords()).
Without that the value would have no <option>, the browser would select the first entry, and the next
save of the settings form would silently adopt it — the defaults changing without anyone touching them.
The stage list is grouped into one <optgroup> per pipeline.
Both fields use the custom property type bcimportedchoice — a pseudo-join that is required as soon as
there is something to choose, so an install that has not imported yet can still save its settings (a plain
required dropdown over an empty table would lock the admin out of saving even the API token).
Two guards keep the pair honest:
- On save —
ConfigboxModelAdminbcsettings::validateData()refuses a default stage that belongs to a different pipeline than the default pipeline. - After every import —
ConfigboxModelAdminbcpipedrivestages::getDefaultsWarnings()reports a default that is unset, missing, retired, hidden from agents, or in the wrong pipeline, and says to set a new one. The warnings are appended to the messages of both import tasks (stages and pipelines lists) and echoed- logged by both import CLIs.
Before the first import the picker is empty and the stage is simply not required (the form stays
submittable). The defaults were seeded to pipeline 1 / stage 1 by 0.5.79 (see below) — a placeholder,
not a curated choice: whatever Pipedrive account this runs against, expect the post-import warning to ask
for a real stage.
Data model (the imported tables)
| Table / column (owning migration) | Meaning |
|---|---|
#__configbox_external_pipedrive_stages (0.5.64) | The imported stages (all pipelines): Pipedrive-owned fields + admin-curated hint / is_won_stage / published / nudge cadence. |
#__configbox_external_pipedrive_pipelines (0.5.67) | The imported pipelines. |
quotes.pipedrive_stage_id (0.5.65) | The chosen Pipedrive pipeline stage (numeric native stage id), nullable. Drives the deal's native stage_id. |
quotes.pipedrive_pipeline_id (0.5.67) | The quote's pipeline (derived from the chosen stage), so the stage resolves even if the deal moves pipeline. |
pipedrive_default_pipeline_id setting (added 0.5.63, renamed by 0.5.67, moved by 0.5.79) | The default pipeline for a quote that has none yet. Lives on #__configbox_external_settings_quote_follow_up. |
pipedrive_default_stage_id setting (0.5.79) | The default stage for a quote with none on file. Same extension table. |
Admin settings
| Setting | Where set | Default | Effect of blank / off |
|---|---|---|---|
pipedrive_default_pipeline_id | admin → ConfigBox settings → Quote Follow-ups (moved out of Pipedrive Integration by 0.5.79) | 1 (seeded) | Dropdown over the imported pipelines. The pipeline a quote with no pipedrive_pipeline_id falls back to. Required once pipelines are imported. Unset → the picker is empty until the quote has a pipeline of its own. |
pipedrive_default_stage_id | admin → ConfigBox settings → Quote Follow-ups | 1 (seeded) | Dropdown over the imported stages, grouped by pipeline. The stage pre-selected for a quote with none on file; must belong to the default pipeline (enforced on save). Required once stages are imported. Unset or not in the rendered pipeline → that pipeline's lowest-order published stage. |
| Imported Pipedrive Stages + Pipelines lists | admin → ConfigBox settings → Quote Follow-ups | — | Not scalar settings — the curated stages and pipelines lists themselves (both embedded in the group). Populated by the single Import pipelines & stages from Pipedrive task on the stages list; admin-curated columns are preserved on re-import. |
Deployment runbook (manual steps)
- Migrations run automatically on the next page load:
0.5.63(default-pipeline setting),0.5.64(stages table),0.5.65(pipedrive_stage_idcolumn),0.5.67(pipelines table +pipedrive_pipeline_idcolumn + setting rename),0.5.79(both defaults onto the follow-up extension table, seeded to pipeline 1 / stage 1, and existing quotes' test pipeline/stage values reset to the same pair). No manual DDL. See migrations.md. - Import — click Import pipelines & stages from Pipedrive on the stages list (one button imports
both, pipelines first). The CLIs
cb_pipedrive_import_pipelines.php/cb_pipedrive_import_stages.phpremain available for scripted/manual use. Until stages are imported the picker is empty (the form stays submittable). Read the messages the import prints — that is where a stale default is reported. - Set the two defaults (Default pipeline + Default stage, Quote Follow-ups group). The seeded pipeline 1 / stage 1 is a placeholder; pick the real pair from the dropdowns. Saving is refused if the stage is not one of the pipeline's.
- Curate the stages: set
hint,is_won_stage,published("Visible to agents"), and the nudge cadence per stage. Re-imports preserve these. - Smoke test: open the landing page for a quote on the pipeline → the picker shows its published stages
with
NN%badges, the configured default stage pre-selected; picking the won-flagged stage marks the quote won.
There is no on/off switch — the picker appears wherever stages are imported for the quote's pipeline.
Testing
Covered by tests/specs/agent/quote-follow-up-variations.spec.ts (the picker, win-probability badge,
won-via-stage, non-won revert) with stages seeded deterministically via support.seedStages(). See the
test catalog.
Related docs
- landing-page.md — the page that renders the picker and persists the choice.
- stage-sync.md — mirrors the chosen stage/pipeline into the quote-stage-sync sheet.
- pipedrive/README.md · pipedrive/admin-manual.md — the imports and the Pipedrive side.