Case Study: Automating Intake and Follow-Up for a Solo Consultant
This is not a story about a tool. It is the description of the pipeline one independent consultant runs between a prospect filling in a web form and a signed scope of work, built so a single person can keep two dozen live conversations alive without a CRM administrator. The interesting design question is not what the model writes; it is where the automation is required to stop and hand the keyboard back.
Start from the brief schema, then design the form
The usual mistake is starting with the form questions. Start with the fields you need in order to write a proposal, then delete every question that does not feed one.
This pipeline’s brief has seven substantive fields. Four are required. Two are nullable, and the normalizer is explicitly forbidden from guessing them.
{
"type": "object",
"additionalProperties": false,
"required": ["client_name", "problem_statement", "success_definition", "decision_maker"],
"properties": {
"client_name": { "type": "string" },
"problem_statement": { "type": "string", "maxLength": 1500 },
"success_definition": { "type": "string", "maxLength": 1500 },
"decision_maker": { "type": "string" },
"deadline": { "type": ["string", "null"], "format": "date" },
"budget_band": { "type": ["string", "null"],
"enum": ["under_10k", "10k_to_25k", "25k_plus", null] },
"out_of_scope_notes": { "type": ["string", "null"] },
"client_words": { "type": "string" }
}
}
The form itself is Tally, Fillout, or a plain HTML form in front of a Cloudflare Worker if you want to own the data. Long-answer boxes get a visible character counter, because a client who writes one line under “what does success look like” is telling you something the proposal should reflect rather than paper over. client_words is the raw concatenation of every free-text answer, kept verbatim and never truncated. It is the raw material for the first section of the draft.
The form also carries fields the model never touches: submission timestamp, referrer, form version. When a question starts producing useless answers, the version number is how you find out which change caused it. If you are still choosing where this runs, the setup in the AI automation guide covers the hosting choices; resist adding a CRM at this stage, for the reasons in the tool sprawl writeup.
One normalization pass, and no cleverness
After submission, a single model call does three things: split compound answers into separate fields, expand obvious acronyms, and copy the raw answer into client_words. It does not summarize. Summaries lose the client’s own phrasing, and that phrasing is what makes the generated proposal read like it was written by someone who actually read the form.
# intake.yaml
model: <your provider, any capable instruction-tuned model>
temperature: 0
response_format: json_schema
schema_ref: ./brief.schema.json
rules:
- never invent a value for a nullable field
- if two answers contradict each other, set needs_review: true
- client_words stays verbatim; no truncation, no cleanup
on_needs_review: tag_thread_and_email_owner
If needs_review is true, the brief goes to a “read this first” folder instead of the proposal queue. Contradictions are common: a six-week deadline alongside a scope that reads like six months, or a decision maker who is not the person who filled in the form. Both are worth your eyes before any text is drafted.
Draft the proposal in sections, not in one prompt
A single prompt asking for a full proposal produces a document with a plausible-sounding middle and no spine. Generate into a Markdown scaffold section by section, then assemble:
- Situation as understood โ built almost entirely from
client_words - What done looks like โ from
success_definition - Scope and not-in-scope โ required fields plus
out_of_scope_notes - Approach and phases โ draftable once you paste in your own standard phase list
- Fees
- Assumptions and client responsibilities
- Next step
Sections 1 through 3 are machine drafts. Section 4 is half-draftable, because the ordering logic is yours but the prose can be generated. Sections 5, 6 and 7 are written by a human every single time. That boundary is the whole design: the model handles recall and formatting, the human handles commitment and price.
Follow-up that stops the moment someone replies
Cadence: day 3, day 7, day 14, then stop. Each message is a send on the same email thread, with In-Reply-To and References headers pointing at the previous message id, and the state lives in SQLite rather than in the mail client.
# followup_tick.py โ run on cron every 15 minutes
STEPS = {1: 3, 2: 7, 3: 14} # step -> days after last send
DUE = """
SELECT s.id, s.thread_id, s.step, s.client_email
FROM sequences s
WHERE s.status = 'active'
AND s.next_send_at <= :now
AND NOT EXISTS (
SELECT 1 FROM messages m
WHERE m.thread_id = s.thread_id
AND m.direction = 'inbound'
AND m.received_at > s.last_send_at
)
"""
def tick(db, now):
for row in db.execute(DUE, {"now": now}):
send_followup(row) # sets In-Reply-To from last outbound message
step = row["step"] + 1
db.execute(
"""UPDATE sequences
SET step = ?, last_send_at = ?, next_send_at = ?, status = ?
WHERE id = ?""",
(step, now, now + STEPS.get(step, 0) * 86400,
"active" if step in STEPS else "exhausted", row["id"]),
)
The stop condition is that NOT EXISTS clause, and it is the part people get wrong. A same-thread check alone is not enough.
| Stop rule | Catches | Misses | Upkeep |
|---|---|---|---|
| Any inbound message on the tracked thread | Normal replies, Gmail and Outlook threading | New-subject replies, replies from a different address, an assistant replying from their own mailbox | One query |
| Inbound sender matches a domain allowlist | Assistants and colleagues on the client domain | Anyone replying from a personal address | Maintain the list |
| Manual review in a daily digest | Whatever the rules missed | Only as fast as you read the digest | One email a day |
Run all three. The thread rule is cheap and handles most cases; the domain rule catches the office manager; the digest is the backstop for the situation you did not imagine.
What broke first, and how it was caught
The stop-on-reply logic. A prospect replied from a new subject line, from her work address, after the original thread had been started from a personal one. The thread check saw no inbound message on the tracked thread. The domain allowlist had a typo in the domain, so it did not match either. The day-7 message went out to someone who had already answered.
What caught it was the digest, not the logic. Every morning the cron wrote one email listing each follow-up sent in the last 24 hours, the query used to decide it was due, and the message id it was threaded to. That list takes two minutes to read. The bad send was visible in it the next morning, and the fix was small: normalize addresses to their domain before matching and treat any thread with overlapping participants as replied. The deeper lesson is that the digest is the monitoring layer. A stop condition you cannot audit is a stop condition you do not have. That is the same reasoning behind the triage habits for failing automations.
A quieter break came earlier. The normalizer filled budget_band with 10k_to_25k when the client had left it blank, and the draft proposal opened with a fee band nobody had agreed to. Nullable fields need an explicit leave-this-null instruction plus a validation step that rejects a non-null value for any field with no matching source text.
What to do next
- Write the brief schema before the form. Mark two fields nullable, and delete every form question that does not feed a field. The schema is the product; the form is a view of it.
- Route contradictions to a
needs_reviewpath and treat that folder as a hard gate in front of the proposal queue. - Split your proposal template into seven sections and mark sections 5, 6 and 7 as human-only. Do not let a generated fee paragraph reach a client.
- Implement all three stop rules from the table above, and log the deciding query and message id with every send. Without the log, the stop condition is a guess.
- Run the cadence in dry-run mode for two weeks before it sends anything: write the messages you would have sent into the digest instead. If you are starting from no technical background, the AI Developer Stack Bundle ($79, one-time) is the entry-level course plus a prompt library โ it will not build this pipeline for you, but it is the cheapest way to get the vocabulary before you wire anything to a live client inbox.
Get AI Starter Bundle
AI Developer Stack Bundle โ $79, one-time payment, instant download. See the full breakdown on the review page.
Related reading
- The Ultimate Guide to AI Automation 2026
- How I Built a 10-Product Digital Business
- Building an AI Content Factory
๐ Want to try before you buy?
Both samples are 100% free. Enter your email to unlock the download links and get our 3-day AI toolkit drip in your inbox.
No spam. Unsubscribe anytime. We respect your privacy.
Or browse the full catalogue in the Slashman Tools storefront.
Published by slashman413 โ writing practical, evergreen guides on money, productivity, developer tooling and the web. More about this site โ
Frequently Asked Questions
Do I need a CRM to run this?
No. SQLite plus one email account covers intake, briefs, sequences and reply state at solo scale. A CRM becomes worth the overhead once more than one person touches the same pipeline.
How do I stop follow-ups if the client replies from a different address?
Match on the normalized domain of every participant in the thread, not just the tracked thread id, and keep a domain allowlist for assistants. Keep the daily digest running as the backstop for anything the rules miss.
Should the model write the whole proposal?
No. Situation, success definition and scope are safe to draft. Fees, assumptions, client responsibilities and the next step are written by a human every time, because those are commitment and price decisions.
๐ Recommended Tools
๐ Related Articles
- Every Product We Sell on Gumroad: The Complete CatalogEvery live Slashman Tools product on Gumroad in one place โ AI prompts, agent workflows, dashboards, courses โฆ
- What Makes a Feishu Template Worth Paying ForA breakdown of Feishu template parts: data model, views, permission scopes, automation flows โ and which ones โฆ
- Buying a Template vs Building Your Own: Variables That DecideA practical decision guide for buying a template versus building your own workspace: data model stability, โฆ
- n8n vs Custom Multi-Agent Workflows: When to Stop Dragging Nodes and Write Code (2026)n8n or a custom code multi-agent workflow? An honest 2026 comparison of versioning, testing, cost, LLM โฆ
๐ฌ Free Weekly AI Product Guides
New tools, templates and automation walkthroughs โ plus hands-on updates on the Slashman Tools catalogue. One email a week, zero fluff.
Free forever ยท No spam ยท Unsubscribe anytime ยท Sent instantly