Skip to content

End-to-End — Preconstruction Lifecycle

StatusShipped
LinearPIN-6 ↗ PIN-143 ↗ PIN-295 ↗ PIN-134 ↗

This is the front door: the whole real-world story of winning (or not winning) a job, told stage by stage, with the business reasoning behind each one — not just what to click. The per-feature walkthroughs are still where the exact click-by-click steps live; this doc links out to them at each stage. Read this one first if you're new to the app or the industry — it's written so a non-technical reader can follow it and understand why something being wrong is actually a problem, not just that it looks different from a spec.

Why this doc exists: the app's own Sample Project A walkthrough is titled "End-to-End" but actually starts at bid creation — skipping the real first step, the Bid Invite. And none of the per-feature docs explain why a given detail matters, only what it does. Two real, currently-open examples that motivated this doc: editing the Bid amount on the estimate sheet silently overwrites the vendor's original quoted price (PIN-605) — only obvious as a problem if you understand that number is supposed to be locked to what the vendor actually quoted. And a generated Proposal currently shows no markup anywhere at all — the total equals raw subcontractor cost (PIN-631, Urgent, blocked on a product decision, not a quick fix) — only obvious as a serious problem if you understand a client receiving that PDF is looking at Pinley's exact cost with zero margin built in.

Prerequisites

  • Role: Estimator or Chief Estimator for the full lifecycle through Award. A PM login is useful at the very end, to see the receiving side.
  • Test data: the test-app-files Drive foldersample-project-A/ has everything for one realistic job. This doc doesn't require it (you can walk Stage 1 with any invite), but it's the fastest way to get real files in hand for Stages 3–5.
  • Permissions cached? Relogin if a button described below is missing.

Stage 1 — Bid Invite

Why this matters

A Bid Invite is Pinley's first contact with a possible job — a GC (directly, or synced in from BuildingConnected/Procore) is asking "will you price this?" Nothing is committed yet. Declining costs nothing; but a wrong Accept — taking on scope Pinley can't actually staff, or missing that a duplicate invite already exists — creates real, wasted downstream work for an estimator who now has to build out an estimate for a job that was never going to happen. This is also the only stage where the GC directly sees Pinley respond: the acknowledgment/decline emails sent from here are Pinley's first impression back to the client, so exact wording matters more than it would on a purely internal screen.

One quirk worth knowing before you test anything here: New → Reviewing isn't a button anyone clicks. It happens automatically, server-side, the moment anyone opens the invite to look at it. If you're trying to verify "does opening an invite mark it Reviewing," you don't need to hunt for a control — just open it and check.

What to do

  1. Bid Invites in the sidebar (/bid-invites). Default view: Inbox, filtered to Pending Review (New + Reviewing combined). Switch the filter to All Statuses to see everything, or toggle to Cards (top-right) for a kanban view.
  2. To bring in real invites instead of manually creating one: toolbar SyncProcore or Building Connected (first-time BuildingConnected use requires an OAuth connect step first).
  3. Click any invite row to open its detail panel.
  4. Accept (Check icon, in the sticky actions bar): opens Accept Invite — Create Bid. Pick a Lead Estimator (optional), a Priority, add Notes, and decide whether to send an acknowledgment email to the GC. Create Bid creates the Bid and takes you straight to it.
  5. Decline (X icon): opens Decline Invite. Reason is required (At Capacity / Scope Mismatch / Location-Travel / Timeline Conflict / GC Relationship / Budget-Margin / Other). Internal Notes are explicitly never sent to the GC. You separately choose whether to send a decline email.

Verify

  • Opening an invite — without clicking anything else — flips it from New to Reviewing on its own.
  • Accept creates exactly one Bid and lands you on it; the invite's detail panel now shows a View Bid link back.
  • Decline requires a reason before it lets you submit.
  • Try declining an invite synced from BuildingConnected/Procore in a way that makes the platform-side reject call fail (or ask me to help simulate this) — you should see a warning toast, not a success one, telling you the decline saved locally but didn't push back to the source platform. That's a real degraded-mode path, not a bug by itself — but a silent success toast in that situation would be.
  • PIN-549 regression check (fixed): running a BuildingConnected sync should never silently move an invite that's already in Reviewing back to New — that's a real status regression a busy estimator could easily miss, since nothing prompts them to notice their invite "reset."

Exit: Declined

A Declined invite is meant to be a dead end — nothing else should ever come of it.

  • It never produces a Bid record.
  • It drops out of the default Pending Review filter but is still findable under All Statuses or Declined — greyed out, not deleted.
  • Its Decline Reason and Internal Notes show on the detail panel; the internal notes were never emailed anywhere, only the optional decline email (if you checked that box) went out.

Stage 2 — Bid

Why this matters

Accepting an invite creates a Bid — Pinley's own record of the effort to win this job. Everything from here forward (documents, vendors, the estimate, the eventual proposal) hangs off this one record. The active-path status pipeline (New → Qualifying → Estimating → Submitted → Awarded) is forward-only on purpose — it exists so a step can't be quietly skipped, like jumping straight into Estimating without the duplicate-project check that Qualifying performs. If you can force a bid backward, or skip a required stage, that's a real data-integrity bug, not a UI inconvenience — it means someone downstream is working from an unchecked assumption.

What to do

The detailed create-bid steps (manual path) live in Sample Project A — this doc picks up assuming a Bid already exists, either from an Accepted invite (Stage 1) or manual creation.

Verify

  • A bid cannot skip directly from New to Estimating — Qualifying must happen first.
  • Awarded/Lost only become selectable from Submitted, matching the real transition map — not offered earlier in the pipeline.
  • PIN-538 regression check (fixed): the status dropdown on creating a bid should never itself offer terminal statuses (Lost, Awarded) — those must only become reachable later, through the real pipeline. A bid that can be born already Awarded defeats the whole point of the forward-only pipeline described above.

Exit: Lost / No-Bid / Withdrawn

Correction worth knowing before you test this: these are not fully terminal, despite how they're sometimes described. Each can be moved to Reopened, and Reopened can go on to Qualifying or Superseded. Don't assume "Lost" is the end of the story — test the Reopen path too, and don't file "can't get out of Lost" as a bug without trying Reopen first.

All three share one modal (title changes per outcome: Mark Bid as Lost / Mark as No Bid / Withdraw Bid), asking for a Reason (the option list differs slightly per outcome) and Notes. Competitor only appears for Lost, and only once "Lost to competitor" is the picked reason — it's optional even then.

A subtlety worth actually understanding, not just clicking past: the modal auto-fills Submitted Price from the bid's existing contract amount — nobody types it in. Final Contract Value is a real field that shows on the Overview afterward, but nothing in this flow ever sets it for Lost/No-Bid/Withdrawn — only an actual Award populates it. If a bug report or a teammate claims "final price gets captured when a bid is marked lost," that's not accurate — only the submitted price is, and even that's just a snapshot of a number that already existed, not something the person recording the outcome entered.

  • The outcome (reason, competitor if applicable, notes) shows in the Outcome Data card on Overview afterward.
  • Submitted Price is populated automatically; Final Contract Value stays empty for these three outcomes specifically.
  • Reopen is actually available and actually changes status from any of the three.
  • Edge case worth trying: the status change and the outcome record are two separate backend calls, not one atomic action. If you can catch a failure between them (a flaky connection, or ask me to help simulate it), a status that changed with no matching Outcome Data card afterward is a real, reportable bug — not cosmetic.
  • PIN-541 regression check (fixed): when Competitor is filled in on a Lost outcome, it should actually persist and show back on Overview afterward — this used to silently not save at all (the backend had nowhere to store it).

Stage 3 — Takeoff

Why this matters

Takeoff is where drawing quantities become numbers Pinley can price. Every downstream cost — materials, labor, the vendor RFP — ultimately traces back to a takeoff measurement. An inaccurate takeoff doesn't just produce a wrong number in one place; it silently understates or overstates the whole bid, which is exactly the kind of error that doesn't get caught until the job is already priced and sent. This is also why provenance (every Estimate line traces back to a specific takeoff row, which traces back to a specific PDF page) exists — if you can't trace a number back to where it came from, you can't tell whether it's wrong because of a bad measurement or a bad push into the Estimate.

What to do

Full click-by-click: Takeoff.

Verify

See the Takeoff walkthrough's own checklist — the one thing worth calling out here specifically: provenance survives every hop. Click a takeoff row's Source pill and confirm it actually deep-links back to the correct PDF page with the markup highlighted, not just to the PDF generally.


Stage 4 — Estimate

Why this matters — the vendor price-lock risk

The Estimate sheet is where a job actually gets priced: Budget (a placeholder) → Bid (the vendor's actual quoted price) → Cut%Cost (what Pinley actually pays). The Bid column is supposed to be locked to what the vendor actually quoted — Conor confirmed this explicitly in a 2026-07-14 meeting: "that bid number is what we're carrying across from a vendor. That shouldn't change." But as of this writing, the Bid cell is still directly editable in the UI (filed as PIN-605, not yet fixed).

Why this is worse than it sounds: if an estimator can freely edit the Bid number, the estimate sheet stops being a reliable record of what a vendor actually promised. That has real contract implications — if a disagreement with a vendor ever comes up, "what did they actually quote us" needs to be an answer the system can still give, not something that got silently overwritten. When you're testing this stage, don't just check that the math computes correctly — check whether the Bid cell can be edited directly, and if it can, that's the bug to file, referencing PIN-605.

What to do

Full click-by-click: Estimate Sheet and Excel Imports (for spreadsheet-driven estimates).

Verify

Money integrity

  • Try to directly edit a Bid cell after a vendor quote has been selected — flag this as still-open against PIN-605 if it succeeds.
  • Cost = Bid × (1 − Cut%) — confirm the math by hand on at least one row.
  • Currency is stored internally as whole cents but must always display as dollars — a number that's 100× too large or too small on screen is a real conversion bug, not a rounding quirk (this exact class of bug shipped once already, PIN-586, since fixed — worth a regression check, not a new report).
  • Edit only a line item inside a section (not the section header itself) — the section's own subtotal must still update and persist. A subtotal that reverts to null/$0 after a refresh, or looks right on the Estimate tab but shows $0 later on a generated Proposal, is a real bug (PIN-585, since fixed — same regression-check note).

Revision integrity — the sharpest edge in this whole app

  • A saved Revision is a true frozen snapshot — change the live estimate afterward, then re-open the old revision, and confirm the old numbers are still what they were.
  • PIN-573 (open, bounced QA→Todo twice already): on a non-active (historical) revision, only the numeric cost/budget inputs are actually guarded. Adding a vendor column, removing one, and selecting a vendor for a line item are not blocked — and they don't just look wrong, they really write. The reason this matters more than a typical missing-guard bug: vendor columns and line items aren't actually snapshotted per revision at all — they live on one shared Estimate record that every revision reads from. So "editing while viewing a locked past revision" isn't cosmetic; it silently mutates the data the active revision is also using. Test specifically: add/remove a vendor column and select a vendor while viewing a historical revision, then switch to the active revision and confirm whether it changed too.
  • PIN-636 (open, Urgent, real data loss — not a display bug): applying a starter Template from Estimate Settings while viewing a locked historical revision is not blocked at all, and unlike the gap above, this one permanently deletes every existing scope line, sub-line, and vendor quote on that revision and replaces them with blank template rows — no lock check exists anywhere in that code path. If you're testing this, don't do it on real data: use a throwaway bid, apply a Template while viewing a non-active revision, and confirm whether the confirmation warning even mentions you're looking at a locked revision (it currently doesn't).
  • Undo/redo: make several edits including footer % changes, undo repeatedly, then redo. Totals and footer values should restore consistently (not "some fields revert, others don't"), and a refresh after undo should show the same state — confirming the database actually saved the undone state, not just the screen.

Save behavior

  • Trigger a save the backend will reject (an invalid value). You should get one clear error toast — not a silent failure, and not a retry loop that keeps resubmitting the same bad value.
  • Two browser sessions editing the same estimate at once — the live-presence indicator should show the other user, and simultaneous edits shouldn't silently overwrite each other.
  • If the CSI scope-line suggestion dropdown doesn't return results for any search — check whether the CSI library is actually seeded in your environment before filing a bug. If it's seeded and still empty, that's worth reporting; if it's not seeded, that's a data-setup gap, not a functional bug.

Stage 5 — Proposal

Why this matters — the markup-consistency risk

The Proposal is the client-facing document — the one artifact in this whole pipeline a GC actually sees the pricing on. It's generated as a frozen snapshot of the active estimate revision, which matters for the same reason revisions matter generally: if you regenerate or reopen an old proposal and it reflects today's estimate instead of what was true when it was generated, that's a real integrity bug, potentially a legal one (a client should be able to trust that the PDF they received is what it says it is).

PIN-631 (open, Urgent) — the current, real state of markup on proposals is worse than "inconsistent," it's absent. A generated proposal's total right now equals raw subcontractor cost — markup doesn't appear anywhere on the document, not the total, not any breakdown. A client receiving this proposal today is looking at Pinley's exact cost with zero margin built in. This is a side effect of fixing PIN-609 (below) — that fix correctly made the proposal total sum the estimate's live section subtotals, but those subtotals are saved before markup is applied client-side, so the number now flowing into proposals has never had markup added at all.

This isn't a quick-patch situation, and don't file a duplicate if you find it — it's explicitly blocked on a product decision with Conor, not an implementation gap: markup can't simply be tacked onto the total (a client could then back-calculate Pinley's exact margin by comparing a breakdown to the total), and not every vendor is marked up the same way, so a flat percentage spread evenly isn't necessarily correct either. When you're testing this stage, the useful thing to verify is that it's still broken exactly this way (a regression check on a known Urgent issue), not to treat it as a fresh discovery.

What to do

Full click-by-click: Proposals.

Verify

  • PIN-631 regression check: compare the proposal PDF's total against the estimate's own Contract Value (which does include markup) — as of this writing they should not match, confirming the gap is still open. If they suddenly do match, that's news — check whether it was fixed with a real decision from Conor on the approach, or just tacked on (which would reopen the back-calculation risk described above).
  • PIN-609 regression check (fixed): the proposal's section-by-section breakdown should sum to the same grand total shown on the PDF — both should come from the estimate's live section subtotals at the moment the revision was generated, not a separate stale snapshot field. If the breakdown and the total ever disagree with each other, that's this bug back.
  • Snapshot semantics: generate a proposal, change the underlying estimate, regenerate/reopen — the old version must still show the old numbers.

Stage 6 — Award

Why this matters

Award is the one truly irreversible step in this whole lifecycle — Awarded has no forward transitions at all (unlike Lost/No-Bid/Withdrawn, which can be Reopened). The moment a bid is Awarded, it becomes a Project, and everything estimating built (the active revision's numbers, the vendors selected, the takeoff provenance) needs to survive the handoff intact — a PM picking up the new project shouldn't have to go back and ask Estimating "wait, what did we actually quote for this?" This is also the point where Final Contract Value actually gets set (unlike the Lost/No-Bid/Withdrawn exit, which never populates it) — worth confirming it's captured correctly here specifically, since that's the one flow that's supposed to set it.

What to do

The Award confirmation itself (Contract Value / Channel / Date) is covered in Sample Project A step 3. The receiving side — what a PM sees once the project exists — is Projects & Project Detail.

Verify

  • Awarding creates exactly one Project (not zero, not duplicates on a retry).
  • The new Project's linked parties (GC, client, address) and contract value match what the Bid had.
  • Final Contract Value is actually set on the Bid's Outcome Data now (contrast with the Lost/No-Bid/Withdrawn exit, where it's never populated).
  • If there are sibling bids on the same project from other GCs, and you use the "mark other GC submissions as Lost" option during Award — check whether those siblings get a real Outcome Data record too, or just a bare status change. This is a secondary path into Lost that doesn't go through the Lost/No-Bid/Withdrawn modal at all, and its outcome-recording behavior hasn't been confirmed — worth a specific test.


Updating this walkthrough

This doc is the narrative spine, not the click-by-click reference — keep the "why" sections current with real product decisions and filed bugs, and let the per-feature walkthroughs carry the exact steps. If a per-feature walkthrough's steps change, this doc's links still work; only update here if the business reasoning changes (a bug gets fixed, a rule changes, a new exit path is added).