Two pictures. The first is what a client goes through, from first visit to retainer. The second is what feeds what: the website, the intake page, the call transcript and the proposal are one chain of data, so the proposal writes itself from what the client already told us.
Two doors in: the module page, or a conversation where we suggest it and send the booking link. Either way the booking hands them the intake page on the spot and by email; they fill it before the call, on the call, or both, and the proposal with the blueprint follows within 30 minutes.
Four lanes, read top to bottom. The client never fills in the same thing twice; the system carries every answer forward into the next document. A person reviews and approves at the three places that matter.
Booking, confirmation and reminders. Intake page autosaves to a shared link, pre-fills from the booking URL, shows the operator live answers and who pressed Send. Call recorded and transcribed. The proposal page renders from one data object. Capture, outcomes and the hygiene digest run on their own once set up.
1. Cal.com webhook creates the client's intake link and sends E2. 2. Transcript → structured call notes (hours spent, tool verdicts, history size, where answers land). 3. Intake record + call notes fill the proposal data object. 4. Signature creates the Asana project and sends the access list.
Reads the proposal before it goes out. The data fills it; the operator checks the verdicts and the price band, then presses send. Same at every gate: the system reports, the client approves.
The CRM owns people, organizations and the deal. A small database owns what the pages render from. Shared answers (people, tools, stack, consent) attach to the organization, not the intake, so a second module's intake opens pre-filled and several modules can share one discovery call. Module-specific answers (outcomes) attach to the intake. Asana owns the work, Stripe the money, the e-sign tool the agreement. Every system writes its id back to the CRM engagement, so the engagement is the one place that links everything.
| Field | Type | Set by | Example |
|---|---|---|---|
| name | text | enrichment; client can correct on the intake | Northshore Studio |
| domain · website | text | booking email → enrichment | northshore.ca |
| size_band | enum | enrichment (LinkedIn / site); replaced by the stated counts once given | 11–50 |
| fte · part_time · contractors · sites | int | intake “Your team” step, pre-filled from the booking if known; drives seats and the price band | 18 · 4 · 6 · 1 |
| industry · type | text · enum | enrichment | agency · for-profit / non-profit / public |
| city · province · timezone | text | enrichment; booking timezone | North Vancouver · BC · America/Vancouver |
| stack | enum | MX records: google / microsoft / mixed / unknown | |
| detected_tools | text[] | website tech + email headers, with confidence | Google Meet (0.9), HubSpot (0.8) |
| voice_stage | 1 · 2 · 3 | mini-diagnostic, else call | 2 |
| lead_source · referred_by | enum · contact → | page / referral / outbound / event | referral · Jordan Lee |
| Field | Type | Set by | Example |
|---|---|---|---|
| first · last · email · phone | text | booking; intake “who else”; call | Dana Mori · dana@northshore.ca |
| title | text | enrichment (LinkedIn), confirmed on call | Managing Director |
| organization → | company id | domain match | |
| engagement_role | enum[] | sponsor / decision maker / tools owner / first user / colleague named | sponsor, decision maker |
| source | enum | booking / intake / call / enrichment / manual | intake (named as “who else”) |
| recording_consent · opt_out | bool · date | house rules step; any time after | true |
| voice_enrolled_at | date | set-up week | |
| url | enrichment |
| Field | Type | Set by | Example |
|---|---|---|---|
| organization → · sponsor → · tools_owner → | ids | booking; intake; call | |
| module · tier | enum | booking link or operator. tier is decided by the module count: 1 → single, 2–4 → combo, 5–11 → blueprint, 12 → 360 | voice-memory · single |
| stage | enum | each step writes its own: booked · intake_started · call_done · proposal_sent · signed · delivering · live · retainer · parked · not_a_fit | proposal_sent |
| booked_at · call_at · call_with | datetime · contact → | Cal.com | Oct 8 10:00 · Kelso |
| intake_url · intake_sent_at | url · datetime | webhook; client presses Send | |
| top_two | text | intake | recap-10-min · ask-anyone |
| wants · haves · not_needed · unsure | int | intake (counts; detail lives in the DB) | 5 · 1 · 1 · 0 |
| tools_count · dont_know_tools | int · bool | intake | 6 · false |
| amount · currency · deposit_pct | money | proposal | 14,500 · CAD · 30 |
| proposal_url · proposal_sent_at · proposal_viewed_at | url · datetime | proposal step; page view beacon | |
| agreement_id · signed_at · deposit_paid_at | ids · datetime | e-sign + Stripe webhooks | |
| start_week · project_id | date · Asana gid | signed step | Oct 20 |
| gate1_at · gate2_at · gate3_at · done_means_pass | date · bool | gate reports | |
| nudge_count · parked_at · parked_reason · not_fit_reason | int · date · text | nudge sequence; operator | |
| next_module · retainer_started_at | enum · date | done-means review; E8 reply |
| Column | Type | Notes | |
|---|---|---|---|
| id · slug · engagement_id | uuid · text · fk | slug is the URL; one intake per engagement, re-runs make a new row with kind = quarterly | |
| module · schema_version | text · int | voice-memory · 1. A new module ships a new catalog, same tables | |
| kind | enum | pre-call / quarterly | |
| status · sent_at · last_edited_at | enum · datetime | open / sent | |
| prefill | jsonb | what the booking + enrichment put in: name, org, time, stack, stage | |
| extra | jsonb | anything a future intake needs that isn’t a column yet |
| Column | Type | Notes | |
|---|---|---|---|
| intake_id · desire_key | fk · text | key from desire_catalog, or null for custom | |
| custom_text | text | “in your own words” entries, verbatim | |
| answer | enum | want / not_needed / have / unsure / skipped | |
| is_top_two | bool | ||
| answered_by · answered_at | enum · datetime | client / operator / colleague — the “you” vs “Kelso” tag | |
| call_note | text | what was said about it on the call (“priority one, 3 h/week”) | |
| go_live_week · verdict | int · enum | set by the proposal: build / keep / parked |
| Column | Type | Notes | |
|---|---|---|---|
| org_id · tool_key · first_seen_in (intake_id) | fk · text · fk | key from tool_catalog, or null. Shared across every module’s intake for this organization | |
| custom_name | text | “Something else” entries | |
| place | enum | online / phone / room / on_person / written | |
| who_uses | enum | everyone / some / one / unknown | |
| on_by_default | enum | yes / no / unknown | |
| recordings_go_to | text | from the call: “stay in Fathom”, “Drive”, “nowhere” | |
| source · answered_by | enum | intake / enrichment / call · client / operator / colleague | |
| verdict | enum | activate / optimize / migrate / delete / keep / not_needed (operator, on the call) | |
| verdict_why · what_changes | text | become the two lines in the proposal’s four-state plan | |
| access_needed · access_owner → | text · contact | become the access list (E6) |
| Column | Type | Notes | |
|---|---|---|---|
| org_id · name · role_text · first_seen_in | fk · text · fk | exactly what the client typed: “Priya, our Google Workspace and the Zoom rooms” | |
| email · phone | text | optional; often filled on the call | |
| invited_at · completed_at | datetime | the two-minute tools step sent and finished | |
| contact_id → | fk | set when promoted to a CRM contact with role tools owner | |
| Also: intake_flags (intake_id, key, value) for things like dont_know_tools, too_small_hint, needs_it_on_call. | |||
| Column | Type | Notes | |
|---|---|---|---|
| id · engagement_id · intake_id | fk | ||
| started_at · duration_min · attendees → | datetime · int · contacts | from the calendar | |
| transcript_ref · recording_ref | text | Otter / Meet ids; never copied into the CRM | |
| why_now | text | Q1, verbatim-ish | |
| hours_per_week · who_spends_it | decimal · text | Q2 — the number the price leans on | |
| first_outcome | desire_key | Q4 | |
| history_hours · history_places | int · text[] | Q8 — sizes the migrate work | |
| outputs_wanted | text[] | Q9 — named products where answers must land | |
| recording_language_today · exclusions | text · text[] | Q10 — seeds the house rules | |
| dropped_tools | text[] | Q7 | |
| people_count · client_facing_count · meeting_types | int · int · text[] | Q12 — price band + seats | |
| three_years | text | the closing question, goes on the proposal cover | |
| parked | desire_key[] | explicit “not this” | |
| extracted_by · reviewed_by | text · contact | model version · the operator who checked it |
| Table | Columns | ||
|---|---|---|---|
| proposals | id · engagement_id · version · module · schema_version · data jsonb (the P object the page renders) · url · status (draft / sent / viewed / accepted / superseded) · price_cad · deposit_pct · sent_at · viewed_at · accepted_at · reviewed_by | ||
| agreements | id · engagement_id · proposal_id · provider · envelope_id · signer_contact_id · signed_at · deposit_invoice_id · invoice2_id · invoice3_id · amounts · payment_status | ||
| projects | id · engagement_id · asana_gid · template_version · start_week · gate1_at · gate1_pass · gate2_at · gate2_pass · gate3_at · gate3_pass · done_means jsonb (one row per test: name, result, measured_at) | ||
| reviews | id · engagement_id · kind (quarterly) · intake_id (the re-run) · now_true desire_key[] · newly_wanted desire_key[] · tools_changed text[] · next_module · reviewed_at | ||
| events | id · engagement_id · type · payload jsonb · at — append-only log of every step (booking.created, intake.sent, proposal.viewed, gate.passed…). This is what writes the CRM stage, so the CRM is never the source of truth. | ||
| Column | Type | Notes | |
|---|---|---|---|
| key · module · schema_version | text | recap-10-min · voice-memory · 1 | |
| title · frustration | text | the two lines on the intake card | |
| needs | text[] | brain, chat, auto, crm, tasks, calendar, email, notetaker — drives the “what you’ll need” list | |
| default_go_live_week · sort · active | int · int · bool | retire or reorder without touching old intakes |
| Column | Type | Notes | |
|---|---|---|---|
| key · name · vendor · place | text · enum | fathom · Fathom · Fathom · online | |
| group · tier | text · enum | Notetakers · common / long_tail | |
| logo_asset | url | the file uploaded in the operator view | |
| access_instructions · access_role | text | “Upgrade to team plan; invite ops@goodops.io as admin” — pre-fills E6 | |
| can_record_by_default · exports_transcripts · api | bool | the facts behind activate / optimize / migrate | |
| detect_signatures | text[] | what enrichment looks for to pre-tick it | |
| status · discontinued_at | enum · date | Limitless Pendant: discontinued 2025-12 |
Short, plain, one link each. Signed by whoever runs the call. E2 and E4 are the two that carry the slice: the intake link and the proposal link. Everything else is a nudge or a note.
| Code | Trigger · timing | Job | Stops when |
|---|---|---|---|
| E1 · E1b · E1c | Inbound email with no booking · same day, +3 d, +7 d | One line with the booking link. The alt version recommends a single tool when it's clearly too small. | Booked, or after E1c |
| E2 | Call booked · within 10 min (webhook) | “You're booked for [time] with [Kelso]. Here's your page.” The intake link, pre-filled. “Fill what you know, skip the rest, forward to IT if you like.” | — |
| E3 | 24 h before the call | Reminder with the meeting link and the intake link; notes what's still blank or says “no need to fill anything, we'll do it live.” | — |
| E4 | Call ends · within 30 min | “Here's the plan we talked through.” The proposal link: blueprint, roadmap, price, e-sign and deposit. “Anything that doesn't match what you said, tell me and I'll change it.” | — |
| E5a · E5b · E5c | Unsigned · +2 d, +5 d (start date held until …), +10 d | Nudge, offer a 10-minute call, then close with the door open. | Signed, or after E5c (stage → parked) |
| E6 | Signed + deposit · within 1 h | Kickoff booking link and the access list, one two-minute line per tool with the owner's name. “Nothing is read before kickoff.” | — |
| E7 | Weekly, Friday, from the Asana project | Three lines: this week, next week, one outcome now true. Need from you: one item or “nothing.” | Project closes |
| E8 | Done-means review passed | Two ways forward: the retainer (daily checks, monthly review, new features, named contact; deposit credited) or the next module. | — |