Delivery
Once a quote is accepted and isActiveProject = true, the expert begins delivering against the milestones defined in the accepted proposal. Each milestone represents a discrete unit of work with its own percentage of the total project value, its own due date, and its own invoice.
Flow
Milestones
Milestones are defined by the expert as part of their proposal and locked in when the proposal is accepted. They represent the phased delivery schedule for the project.
Milestone Types
| Type | Description |
|---|---|
time | The milestone is triggered on a specific due date — useful for scheduled deliverables like sprint reviews or monthly reports |
event | The milestone is triggered when the expert marks a specific delivery as complete — useful for feature delivery or project phases |
Milestone Constraints
- The
percentagevalues of all milestones on a proposal must sum to exactly 100% — enforced byMilestone.validateMilestonePercentages(proposalId) - Milestones are ordered via the
orderfield (minimum 1) - Each milestone must have a unique order within its proposal
- Each milestone is linked to exactly one invoice once work begins
Milestone Fields
| Field | Type | Constraints | Description |
|---|---|---|---|
name | String | 3–100 chars, required | Milestone label |
description | String | Max 1000 chars | What will be delivered |
type | String | time or event | Trigger type |
order | Number | ≥ 1, required | Delivery sequence |
percentage | Number | 0–100, whole number | Share of total project value |
status | String | upcoming | in-progress | blocked | done, default upcoming | Delivery state shown to the customer |
owner | String | Expert | Your team | Customer, default Expert | Who is accountable for the milestone |
outcome | String | Max 200 chars, optional | Customer-facing outcome label (falls back to name) |
blocksGoLive | Boolean | Default false | Whether this milestone blocks go-live |
dueDate | Date | Optional | Expected completion date |
proposal | Proposal ref | Required | Parent proposal |
expert | User ref | Required | Responsible expert |
invoice | Invoice ref | Set on completion | Associated invoice |
Delivery status
The expert drives delivery by advancing each milestone’s status from upcoming → in-progress → done. If work stalls, the expert can move an in-progress milestone to blocked and back to in-progress once the blocker clears (a loop transition). The milestone sub-workflow has no verification or evidence step. Reaching done is the trigger that marks a milestone complete in the flow above, and it is distinct from the invoice-derived isPaid / isApproved virtuals below — a milestone can be delivered (status = done) before its invoice is settled.
The expert advances a milestone’s status through an authenticated, tenant- and expert-scoped route — PATCH /api/milestones/[milestoneId]/status with a { status } body. The change is reflected on the customer’s dashboard on the next read: project progress shown to the customer is the percentage-weighted sum of done milestones (which, since percentages sum to 100, reaches 100% when every milestone is done).
Customer action items
Delivery often depends on the customer doing something — approving integration access, confirming a data mapping, signing off a phase. The delivery team (Expert, CSM, or SDM) raises these as action items on the project, and the customer sees and clears them from the “Your actions” strip on their dashboard.
Each action item is scoped to a project (lead) and the customer reads only their own active project’s open items.
| Field | Type | Constraints | Description |
|---|---|---|---|
label | String | Max 200 chars, required | What the customer must do |
dueDate | Date | Required | When it’s needed by |
urgency | String | urgent | upcoming | Drives ordering and emphasis on the dashboard |
status | String | open | completed, default open | Open items show on the dashboard; completing removes them |
blocksGoLive | Boolean | Default false | Whether clearing this item gates go-live |
Open items are sorted by urgency then due date, with dueInDays derived at read time. Completing an item — from the dashboard or the team surface — records who completed it and when, and drops it from the customer’s list.
Change control
During delivery a project’s scope sometimes shifts in ways that move the timeline or cost — an integration expanded, a phase brought forward. The delivery team (Expert, CSM, or SDM) records these as change-control entries on the project, and the customer sees them in the “Changes to your project” section of their dashboard, newest first — a transparent log of how and why the plan changed.
Each entry is scoped to a project (lead) and the customer reads only their own active project’s entries. Impacts are stored as signed numbers (0 = no change) so the dashboard can format them (“+2 days”, ”+£500”) and render the cost in the project’s currency.
| Field | Type | Constraints | Description |
|---|---|---|---|
change | String | Max 500 chars, required | What changed |
timelineImpactDays | Number | Signed, default 0 | Days added or removed; 0 shows as “No change” |
costImpactAmount | Number | Signed, default 0 | Cost added or removed in the project currency; 0 shows as “No change” |
at | Date | Defaults to now | When the change was recorded |
Entries are recorded explicitly by the team — they are not auto-derived from milestone or scope edits — and the customer view is read-only (no approval workflow).
Customer activity feed
Throughout delivery the customer sees a running feed of what has happened on their project, drawn from the platform’s Activity audit log. The dashboard’s latest-update panel shows the most recent events, and a full feed lives at /customer/activity, newest first.
The feed is deliberately customer-facing: only status changes and creation events — a status reached, or a project, milestone, invoice or proposal record created — are shown. Internal field edits, deletions, assignments and comments are filtered out. Each entry is currently tagged No impact; go-live impact is computed separately and is not part of this feed.
Project messaging
Throughout delivery the customer and their delivery team share a single message thread scoped to the project. The customer dashboard’s “Talk to your team” card shows the latest message and a waiting on you / waiting on team signal derived from who sent last, and the full thread lives at /customer/messages, where the customer reads the history and posts a new message.
Each message is scoped to the project (lead) and carries its sender, a senderRole of customer or team, and its text. When the customer posts, every delivery-team participant (the project’s expert and delivery manager) is notified in real time over the platform’s existing Ably channel. The customer reads and posts only on their own active project; non-participants cannot reach the thread.
| Field | Type | Constraints | Description |
|---|---|---|---|
sender | User ref | Required | The user who wrote the message |
senderName | String | Max 200 chars, required | Display name shown in the thread and preview |
senderRole | String | customer | team | Which side of the thread the message is from |
text | String | Max 4000 chars, required | The message body |
blocker | Blocker ref | Optional | The go-live blocker this message addresses, when it is CSM outreach about a specific blocker |
On the delivery side, a CSM working the Go live control dashboard can now send outreach on a specific go-live blocker — a message to the blocking customer, tied to that blocker. The blocker’s conversation card shows the latest message, whether the customer has replied since the CSM’s last outreach (response pending / responded), and when the CSM last made contact. Outreach reuses this same thread — the CSM posts as team, tagged with the blocker it addresses, rather than a parallel collection — and is scoped to the CSM’s portfolio. Chasing non-customer blocking parties (expert, vendor or partner) follows in a later feature.
Computed project health
The customer dashboard’s headline signals — the status-summary boxes (on track / minor issues / at risk), the go-live strip (status, days to go-live, go-live date, delivery confidence, and the reasons behind any risk) and the risk badge (Low / Medium / High) — are computed by the platform, not entered by anyone. They derive deterministically from the project’s real milestones, blockers, due dates and invoices, so the customer always sees an honest read on whether their project will land on time.
The engine computes a single worst-signal-wins severity for the customer’s active project, then maps that one score to every badge so they can never disagree:
| Severity | Triggered when | Overall status | Risk | Go-live confidence | Go-live status |
|---|---|---|---|---|---|
| 2 | A go-live-blocking milestone is overdue (blocksGoLive, past its due date, not done), or an unpaid invoice is overdue | At risk | High | Low | At risk |
| 1 | A non-blocking milestone is overdue, or at least one blocker is open | Minor issues | Medium | Medium | On track |
| 0 | None of the above | On track | Low | High | On track |
The go-live date and the days-to-go-live count come from the project’s planned end date (lead.endDate). Each contributing signal adds a plain-language line to the reasons list shown on the go-live strip (for example, “Milestone ‘Core API build’ is overdue and blocks go live” or “Open blocker: waiting on API key”), and the headline delta is a derived label — “On track, no change”, “Minor issues — being monitored”, or the top reason when at risk.
Because the bands fall out of a pure function (computeProjectHealth), the same inputs always produce the same health, and the thresholds above are reproducible by inspection. Health is read-only for the customer — there is no manual override.
Computed Values
Three virtual fields are derived from the milestone’s associated invoice — these track billing and are separate from the delivery status field above:
| Virtual | Source | Description |
|---|---|---|
amount | proposal.price × (percentage / 100) | Monetary value of this milestone |
isPaid | invoice.paid | Whether the milestone invoice has been settled |
isApproved | invoice.isApproved | Whether the milestone invoice has been approved |
Delivery Completion
A project is considered delivered when:
- All milestones have been marked complete (
status = done) - All associated invoices have been paid (
invoice.paid = true)
At this point the lead is marked as Delivered and the platform issues a CSAT survey to the customer. CSAT scores feed into vendor reporting and expert performance metrics.
Closed Lost
If a project is terminated before completion — due to a dispute, quality issue, or mutual agreement — the lead is marked Closed Lost. This is tracked as a negative transition in the lead’s status workflow and reported separately in the vendor funnel dashboard.