Closed-Won to Kickoff in One Workflow
Short answer: trigger on the CRM stage change to won, key everything on the deal id so a retried webhook can't create a second client, generate a handoff brief from the deal notes with one LLM call, create the client record, folder and task checklist, then put the welcome email and kickoff invite in a review queue instead of sending them blind. Track each step in a onboarding_runs table so a failure halfway resumes instead of starting over.
Why this keeps coming up
Automation communities keep asking the same two questions: what should go into the handoff when a deal closes, and should a small business build dashboards or automations first?
The recurring complaints: sales knows things delivery never hears, the welcome email goes out twice when the CRM retries, someone forgets to create the shared folder, and a half-failed run leaves a client in three systems but missing from the fourth.
What goes into the handoff
| Step | Output | Idempotency key |
|---|---|---|
| 1. Intake | Deal id, company, contacts, value, notes | deal_id |
| 2. Handoff brief | Goals, scope, promises sales made, risks, open questions | deal_id:brief |
| 3. Client record | Row in your client table or project tool | deal_id:client |
| 4. Shared folder | Drive/Notion page from a template | deal_id:folder |
| 5. Task checklist | Kickoff, access requests, invoice, first deliverable | deal_id:tasks |
| 6. Welcome email + kickoff invite | Draft for review, not auto-sent | deal_id:welcome |
| 7. Internal ping | One Slack/Teams message with the brief | deal_id:ping |
Schema
CREATE TABLE onboarding_runs (
deal_id TEXT PRIMARY KEY,
status TEXT NOT NULL DEFAULT 'started', -- started | done | failed
steps_done TEXT[] NOT NULL DEFAULT '{}',
brief JSONB,
started_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
Claim the deal once:
INSERT INTO onboarding_runs (deal_id) VALUES ($1)
ON CONFLICT (deal_id) DO NOTHING
RETURNING deal_id;
No row back means another run already owns this deal, so the workflow only resumes the steps missing from steps_done.
Mark a step done right after it succeeds:
UPDATE onboarding_runs
SET steps_done = array_append(steps_done, $2), updated_at = NOW()
WHERE deal_id = $1 AND NOT ($2 = ANY(steps_done));
The handoff brief prompt (one call, structured output)
Ask the model for strict JSON with these fields: goals, in_scope, out_of_scope, promises_made, deadlines, stakeholders, risks, open_questions. Tell it to write "unknown" instead of guessing. The promises_made field is the one delivery teams thank you for.
Workflow outline (n8n)
- Webhook or CRM trigger filtered to stage = won.
- Postgres claims
deal_id; an IF node routes new deals to the full path and existing ones to resume. - LLM node writes the brief, stored in
brief. - Each creation step runs behind an IF that checks
steps_done, then appends its step name. - Welcome email and kickoff invite are saved as drafts for a human to approve.
- A daily schedule trigger lists runs stuck in
startedfor more than 2 hours.
Dashboards first or automations first?
Automate the handoff first. It is the step where information is lost, and the onboarding_runs table becomes your first dashboard for free: deals won, onboarding started, stuck runs.
Where to run it
- Self-host n8n plus Postgres on Railway or start on n8n Cloud.
- In a visual builder, the same keys work in Make using a Data Store keyed by
deal_id.
FAQ
What if the CRM sends "won" twice after someone reopens the deal? The deal_id claim blocks a second onboarding. If you want re-onboarding, key on deal_id plus the close date.
Should the welcome email ever auto-send? Only after you trust the template; even then, keep the deal_id:welcome step check in front of the send so retries can't double-send.
Where does the brief live? Store it in the run row and paste it into the client folder, so delivery reads it before kickoff.
If you'd rather build this in a visual builder, you can start free on Make.
Disclosure: Some links are affiliate links — I may earn a commission at no extra cost to you.
Top comments (2)
Keying every step on deal_id and listing the keys in one table makes the retry story easy to follow. I see one gap in the claim step. When ON CONFLICT returns no row, the second run goes straight to resume, but the first run may still be in the middle of its steps. CRMs tend to retry exactly then, when the first request is slow to answer. Both runs read the same steps_done, both see the folder step missing, and both create a folder, because a step is only appended after it succeeds. The claim gives you a unique row, not exclusive ownership. A lease closes that: add a locked_until column, and let a run continue only if an UPDATE that pushes locked_until a few minutes ahead, guarded by locked_until being null or in the past, returns a row. For the external creates, looking up by deal_id before creating also covers the case where the folder was made but the mark failed.
ON CONFLICT ... DO NOTHING RETURNING deal_iddoesn't distinguish a failed run from one that's still active. If A has claimed the row and is creating the folder, B can take the resume branch before A records that step insteps_done. Both then attempt the folder step; a duplicate is possible unless that destination actually enforces the listed idempotency key.I'd make the existing-deal path stop for an active owner, with a separate ownership claim for resuming a stopped run. The missing step marker by itself isn't proof that the previous execution has stopped.