docs/skills/letter-review-pipeline/SKILL.md
Complete reference for the Talk to My Lawyer attorney review pipeline. Covers the full review workflow from payment unlock through attorney claim, inline editing, approval/rejection, PDF generation, and subscriber notification. Use when building, debugging, or extending the letter review center, attorney dashboard, or approval workflow.
npx skillsauth add jamilahmedansari/www.talk-to-my-lawyer.com- letter-review-pipelineInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The letter review pipeline handles everything that happens after the AI generates a letter draft and the subscriber pays to unlock it. It covers the attorney review workflow, inline editing, approval/rejection decisions, PDF generation, and all associated notifications.
⚠️ Schema Changes: All schema changes must be applied via Drizzle migrations. Follow the
drizzle/migrations/000X_description.sqlnaming convention.
generated_locked (AI draft complete)
│
▼
[Payment Unlock]
│ First letter free → billing.freeUnlock
│ Pay-per-letter ($200) → Stripe checkout → webhook
│ Subscription → bypasses paywall
│
▼
pending_review
│ Letter appears in Attorney Review Queue
│ Email sent to attorney team
│
▼
[Attorney Claims Letter]
│ review.claim → assigns reviewer
│ Status: pending_review → under_review
│ Subscriber notified ("Attorney reviewing your letter")
│
▼
under_review
│ Attorney sees: intake panel, attachments, AI draft, research
│ Attorney can: edit inline, save edits, add notes
│
▼
[Attorney Decision]
├── APPROVE → approved
│ │ Creates final_approved version
│ │ Generates PDF via PDFKit
│ │ Uploads PDF to S3
│ │ Sends approval email with PDF link
│ │ Creates in-app notification
│ ▼
│ Subscriber downloads PDF from "My Letters"
│
├── REJECT → rejected
│ │ Logs rejection reason (internal + user-visible)
│ │ Sends rejection email
│ │ Creates in-app notification
│ ▼
│ Terminal state
│
└── REQUEST CHANGES → needs_changes
│ Logs change request (internal + user-visible)
│ Sends "changes needed" email
│ Optionally re-triggers AI pipeline
▼
Can loop back to researching or drafting
File: server/routers.ts → billing.*
| Procedure | Condition | Transition |
|-----------|-----------|------------|
| billing.freeUnlock | First letter, no prior unlocked letters | generated_locked or generated_unlocked → pending_review |
| billing.payToUnlock | Creates $200 Stripe checkout | Stripe webhook → pending_review |
| billing.createAttorneyReviewCheckout | Creates $100 Stripe checkout for free-trial letters | Stripe webhook → pending_review |
The Stripe webhook handler (server/stripeWebhook.ts) processes checkout.session.completed events and transitions the letter status.
File: server/routers.ts → review.*
All review procedures require attorneyProcedure guard (role: attorney or admin).
Procedure: review.claim
Input: { letterId: number }
Transition: pending_review → under_review
What happens:
pending_review statusclaimLetterForReview(letterId, reviewerId) — idempotent, rejects if already claimed by anotherclaimed_for_reviewProcedure: review.saveEdit
Input: { letterId: number, content: string (min 50 chars), note?: string }
No status transition (stays under_review)
What happens:
letter_version record (type: attorney_edit)attorney_edit_saved{ versionId }Attorneys can save multiple edits before making a final decision.
Procedure: review.approve
Input:
{
letterId: number;
finalContent: string; // min 100 chars
internalNote?: string; // visible only to staff
userVisibleNote?: string; // visible to subscriber
}
Transition: under_review → approved
What happens:
under_review statusletter_version (type: final_approved) with finalContentcurrentFinalVersionId pointer on letter requestapprovedapproved (internal note)userVisibleNote provided, log additional attorney_note action (user-visible)generateAndUploadApprovedPdf() (non-blocking)letter_requests.pdfUrlProcedure: review.reject
Input:
{
letterId: number;
reason: string; // min 10 chars, internal
userVisibleReason?: string; // shown to subscriber (defaults to reason)
}
Transition: under_review → rejected
What happens:
under_review statusrejectedrejected (internal)rejection_notice (user-visible reason)Procedure: review.requestChanges
Input:
{
letterId: number;
internalNote?: string;
userVisibleNote: string; // min 10 chars
retriggerPipeline: boolean; // default: false
}
Transition: under_review → needs_changes
What happens:
under_review statusneeds_changesrequested_changes (internal)changes_requested (user-visible)retriggerPipeline: true, call retryPipelineFromStage(letterId, intakeJson, "drafting") asyncFile: server/pdfGenerator.ts
Triggered on approval. Produces a professional legal letter PDF with:
Approve action
│
▼
generateAndUploadApprovedPdf()
│ generatePdfBuffer() → PDFKit in-memory
│ storagePut(fileKey, buffer, "application/pdf") → S3
▼
Return { pdfUrl, pdfKey }
│
▼
updateLetterPdfUrl(letterId, pdfUrl)
PDF generation is non-blocking: if it fails, the approval still succeeds. The error is logged but does not roll back the approval.
File: server/email.ts
All review actions trigger both email and in-app notifications:
| Action | Email Function | Notification Type |
|--------|---------------|-------------------|
| Claim | sendLetterUnderReviewEmail | letter_under_review |
| Approve | sendLetterApprovedEmail | letter_approved |
| Reject | sendLetterRejectedEmail | letter_rejected |
| Request Changes | sendNeedsChangesEmail | needs_changes |
| Assign (admin) | sendNewReviewNeededEmail | N/A |
In-app notifications are stored in the notifications table and displayed via notifications.list procedure.
Table: review_actions
Every review operation creates an audit record via logReviewAction():
{
letterRequestId: number;
reviewerId?: number;
actorType: "system" | "subscriber" | "employee" | "admin" | "attorney";
action: string; // claimed, approved, rejected, etc.
noteText?: string;
noteVisibility: "internal" | "user_visible";
fromStatus?: string;
toStatus?: string;
createdAt: timestamp;
}
Visibility rules:
internal: Only visible to attorneys and adminsuser_visible: Visible to the subscriber who owns the letterTable: letter_versions
Versions created during review:
| Version Type | Created By | When |
|-------------|-----------|------|
| ai_draft | system | Pipeline completion (Stage 3) |
| attorney_edit | attorney | Each review.saveEdit call |
| final_approved | attorney | On review.approve |
The letter request tracks two version pointers:
currentAiDraftVersionId → latest AI-generated draftcurrentFinalVersionId → approved version (set on approval)File: server/routers.ts → admin.*
| Procedure | Purpose |
|-----------|---------|
| admin.assignLetter | Assign a letter to a specific employee/attorney |
| admin.forceStatusTransition | Override status to any value |
| admin.getLetterDetail | Full letter detail with all versions, actions, jobs |
| admin.retryJob | Re-trigger pipeline from a specific stage |
| Page | Route | Role | Purpose |
|------|-------|------|---------|
| Attorney Dashboard | /attorney | attorney, admin | Overview stats |
| Review Queue | /attorney/queue | attorney, admin | List of letters to review |
| Review Detail | /attorney/:id | attorney, admin | Full review interface |
| Letter Detail | /letters/:id | subscriber | View letter status, download PDF |
| Admin Letter Detail | /admin/letters/:id | admin | Full audit view |
| File | Purpose |
|------|---------|
| server/routers.ts (L380-622) | All review.* procedures |
| server/routers.ts (L625-724) | All admin.* procedures |
| server/routers.ts (L762-991) | All billing.* procedures |
| server/pdfGenerator.ts | PDFKit PDF generation |
| server/email.ts | Email notification templates |
| server/db.ts (L229-460) | Review-related DB helpers |
| server/stripeWebhook.ts | Stripe payment webhook |
| server/storage.ts | S3 file upload |
references/review-actions.mdreferences/pdf-generation.mdreferences/payment-flow.mdBefore beginning any task in this skill, initialize with the active frontend issue register (agentId: a28925291b7edfefb).
as any, enforce typed email verification checks, improve localStorage and upload/submission error handling, and remove non-SPA redirects.development
Complete reference for the Talk to My Lawyer AI letter generation pipeline. Covers the 3-stage orchestrator (Perplexity research → Anthropic draft → Anthropic assembly), intake normalization, status machine, database writes, error handling, and n8n fallback path. Use when building, debugging, or extending the letter generation system.
development
Comprehensive AI assistant instructions for Talk to My Lawyer platform. Covers 3-stage AI pipeline (Perplexity research → Anthropic draft → Anthropic assembly), review workflow, payment processing, database operations, frontend patterns, and all coding conventions. Optimized for AI code generation assistants.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.