Freelancers lose 10-15 hours a week to proposals, status reports, and invoice chasing. That is billable time gone.
These 5 Python scripts use OpenAI's structured outputs and Pydantic models to generate real client deliverables — not chat summaries. Each script runs locally, costs under $0.01 per call, and produces documents you can send to clients today.
The Stack
Every script uses the same three dependencies:
pip install openai python-docx Jinja2
-
openai (v2.28.0):
chat.completions.parse()with Pydantic models for structured output -
python-docx (v1.2.0): generates
.docxfiles clients can open in Word or Google Docs - Jinja2 (v3.1.x): templates for emails and HTML reports
The core pattern is the same in every script: define a Pydantic model for the output shape, call client.chat.completions.parse(), and pipe the structured result into a document generator.
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from env
class MyOutput(BaseModel):
title: str
sections: list[str]
completion = client.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a document generator."},
{"role": "user", "content": "Generate a project summary."},
],
response_format=MyOutput,
)
result = completion.choices[0].message.parsed
print(result.title) # typed, validated, no regex needed
gpt-4o-mini costs $0.15 per million input tokens and $0.60 per million output tokens as of March 2026. A 500-word proposal generation costs roughly $0.002.
Script 1: Project Proposal Generator
Clients expect proposals within 24 hours. This script takes a project description and generates a structured proposal with scope, timeline, deliverables, and pricing.
from pydantic import BaseModel
from openai import OpenAI
from docx import Document
client = OpenAI()
class Milestone(BaseModel):
name: str
duration_days: int
deliverables: list[str]
class Proposal(BaseModel):
project_name: str
executive_summary: str
scope: list[str]
out_of_scope: list[str]
milestones: list[Milestone]
total_duration_days: int
assumptions: list[str]
def generate_proposal(brief: str, hourly_rate: float) -> Proposal:
completion = client.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"You are a senior freelance consultant. "
"Generate a professional project proposal. "
"Be specific about deliverables. "
"Keep scope items measurable."
),
},
{"role": "user", "content": brief},
],
response_format=Proposal,
)
return completion.choices[0].message.parsed
def save_proposal_docx(proposal: Proposal, path: str):
doc = Document()
doc.add_heading(f"Proposal: {proposal.project_name}", level=1)
doc.add_paragraph(proposal.executive_summary)
doc.add_heading("Scope", level=2)
for item in proposal.scope:
doc.add_paragraph(item, style="List Bullet")
doc.add_heading("Out of Scope", level=2)
for item in proposal.out_of_scope:
doc.add_paragraph(item, style="List Bullet")
doc.add_heading("Timeline", level=2)
for ms in proposal.milestones:
doc.add_paragraph(
f"{ms.name} ({ms.duration_days} days)", style="List Number"
)
for d in ms.deliverables:
doc.add_paragraph(d, style="List Bullet 2")
doc.add_heading("Assumptions", level=2)
for a in proposal.assumptions:
doc.add_paragraph(a, style="List Bullet")
doc.save(path)
# Usage
proposal = generate_proposal(
brief="Build a REST API for inventory management with auth, CRUD, and reporting",
hourly_rate=150.0,
)
save_proposal_docx(proposal, "proposal.docx")
The Pydantic model guarantees every proposal has the same structure. No more forgetting the "out of scope" section.
Script 2: Weekly Status Report
Clients want updates. Writing them is tedious. This script takes raw notes — bullet points, Slack messages, commit logs — and produces a formatted status report.
from pydantic import BaseModel
from openai import OpenAI
from docx import Document
client = OpenAI()
class StatusReport(BaseModel):
week_ending: str
completed: list[str]
in_progress: list[str]
blocked: list[str]
next_week: list[str]
risks: list[str]
hours_logged: float
def generate_status(raw_notes: str, project_name: str) -> StatusReport:
completion = client.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"You are a project manager. "
"Extract a structured weekly status report from raw notes. "
"Categorize items accurately. "
"Flag any risks or blockers."
),
},
{
"role": "user",
"content": f"Project: {project_name}\n\nNotes:\n{raw_notes}",
},
],
response_format=StatusReport,
)
return completion.choices[0].message.parsed
def save_status_docx(report: StatusReport, project: str, path: str):
doc = Document()
doc.add_heading(f"Status Report: {project}", level=1)
doc.add_paragraph(f"Week ending: {report.week_ending}")
doc.add_paragraph(f"Hours logged: {report.hours_logged}")
for section, items in [
("Completed", report.completed),
("In Progress", report.in_progress),
("Blocked", report.blocked),
("Next Week", report.next_week),
("Risks", report.risks),
]:
doc.add_heading(section, level=2)
for item in items:
doc.add_paragraph(item, style="List Bullet")
doc.save(path)
# Usage
notes = """
- finished auth endpoints monday
- started reporting module wednesday
- waiting on client for design specs
- database migration took longer than expected
- need to test payment integration next
- about 32 hours this week
"""
report = generate_status(notes, "Inventory API")
save_status_docx(report, "Inventory API", "status-report.docx")
32 hours of scattered notes become a clean Word document in 3 seconds.
Script 3: Invoice Line Item Extractor
Turning time logs into invoices is error-prone. This script reads a text dump of time entries and structures them into billable line items.
from pydantic import BaseModel
from openai import OpenAI
client = OpenAI()
class LineItem(BaseModel):
description: str
hours: float
rate: float
amount: float
class Invoice(BaseModel):
client_name: str
invoice_period: str
line_items: list[LineItem]
subtotal: float
tax_rate: float
tax_amount: float
total: float
def extract_invoice(time_log: str, client_name: str, rate: float) -> Invoice:
completion = client.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
f"You are an invoice generator. "
f"The hourly rate is ${rate}. "
f"Group related entries into line items. "
f"Calculate amounts as hours * rate. "
f"Use 0% tax rate unless specified. "
f"Subtotal = sum of all line item amounts. "
f"Total = subtotal + tax_amount."
),
},
{
"role": "user",
"content": (
f"Client: {client_name}\n\nTime log:\n{time_log}"
),
},
],
response_format=Invoice,
)
return completion.choices[0].message.parsed
# Usage
time_log = """
Mon 3h - API endpoint development
Mon 1h - code review with team
Tue 4h - database schema design
Wed 2h - bug fixes on auth module
Thu 3h - reporting feature
Fri 2h - deployment and testing
"""
invoice = extract_invoice(time_log, "Acme Corp", rate=150.0)
for item in invoice.line_items:
print(f" {item.description}: {item.hours}h x ${item.rate} = ${item.amount}")
print(f"Total: ${invoice.total}")
The Pydantic model enforces that every invoice has line items, a subtotal, tax, and total. The LLM handles the grouping logic — merging "API endpoint development" and "bug fixes on auth module" into a single "Backend Development" line item if you want.
Script 4: Client Email Drafter
Cold outreach and follow-up emails eat time. This script generates context-aware emails using Jinja2 templates for consistent formatting.
from pydantic import BaseModel
from openai import OpenAI
from jinja2 import Template
client = OpenAI()
class EmailDraft(BaseModel):
subject: str
greeting: str
body_paragraphs: list[str]
call_to_action: str
sign_off: str
def draft_email(context: str, email_type: str) -> EmailDraft:
completion = client.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
f"You are a professional freelance consultant. "
f"Draft a {email_type} email. "
f"Be concise — under 200 words total. "
f"Professional but not stiff. "
f"Include a clear call to action."
),
},
{"role": "user", "content": context},
],
response_format=EmailDraft,
)
return completion.choices[0].message.parsed
EMAIL_TEMPLATE = Template("""Subject: {{ email.subject }}
{{ email.greeting }}
{% for paragraph in email.body_paragraphs %}
{{ paragraph }}
{% endfor %}
{{ email.call_to_action }}
{{ email.sign_off }}
""")
def render_email(email: EmailDraft) -> str:
return EMAIL_TEMPLATE.render(email=email)
# Usage
email = draft_email(
context=(
"Follow up with Sarah at TechStart. "
"We delivered the MVP last week. "
"Want to discuss phase 2 — mobile app."
),
email_type="follow-up",
)
print(render_email(email))
The structured output means every email has a subject, greeting, body, CTA, and sign-off. The Jinja2 template ensures consistent formatting across every email you send.
Script 5: Scope Document Generator
Scope creep kills freelance projects. This script generates a formal scope document that both you and the client sign off on before work begins.
from pydantic import BaseModel
from openai import OpenAI
from docx import Document
client = OpenAI()
class Requirement(BaseModel):
id: str
description: str
priority: str # "must-have", "should-have", "nice-to-have"
acceptance_criteria: list[str]
class ScopeDocument(BaseModel):
project_name: str
objective: str
requirements: list[Requirement]
exclusions: list[str]
change_process: str
estimated_hours: int
def generate_scope(description: str) -> ScopeDocument:
completion = client.chat.completions.parse(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
"You are a senior technical consultant. "
"Generate a formal scope document. "
"Each requirement needs a unique ID (REQ-001, REQ-002...). "
"Every requirement must have measurable acceptance criteria. "
"Include a change process section."
),
},
{"role": "user", "content": description},
],
response_format=ScopeDocument,
)
return completion.choices[0].message.parsed
def save_scope_docx(scope: ScopeDocument, path: str):
doc = Document()
doc.add_heading(f"Scope: {scope.project_name}", level=1)
doc.add_paragraph(f"Objective: {scope.objective}")
doc.add_paragraph(f"Estimated hours: {scope.estimated_hours}")
doc.add_heading("Requirements", level=2)
for req in scope.requirements:
doc.add_paragraph(
f"[{req.id}] {req.description} ({req.priority})",
style="List Number",
)
for ac in req.acceptance_criteria:
doc.add_paragraph(f"AC: {ac}", style="List Bullet 2")
doc.add_heading("Exclusions", level=2)
for exc in scope.exclusions:
doc.add_paragraph(exc, style="List Bullet")
doc.add_heading("Change Process", level=2)
doc.add_paragraph(scope.change_process)
doc.save(path)
# Usage
scope = generate_scope(
"E-commerce platform: product catalog, shopping cart, "
"Stripe checkout, admin dashboard, email notifications"
)
save_scope_docx(scope, "scope-document.docx")
The requirement IDs and acceptance criteria are the difference between "build me an e-commerce site" and a document that protects you from scope creep.
The Pattern Behind All 5 Scripts
Every script follows the same 3-step pattern:
- Define the output shape with a Pydantic model
-
Call
client.chat.completions.parse()withresponse_format=YourModel - Pipe the typed result into a document generator (python-docx or Jinja2)
No regex. No JSON parsing. No try/except json.JSONDecodeError. The OpenAI SDK handles schema enforcement at the API level.
The Pydantic model does double duty: it tells the LLM what structure to produce AND it validates the response on your end. If the model returns malformed data, Pydantic catches it before it reaches your document generator.
Cost Reality Check
Running all 5 scripts once per week with gpt-4o-mini:
| Script | Avg tokens | Cost per run |
|---|---|---|
| Proposal | ~1,200 | $0.002 |
| Status report | ~800 | $0.001 |
| Invoice | ~600 | $0.001 |
| ~500 | $0.001 | |
| Scope doc | ~1,500 | $0.003 |
| Weekly total | ~$0.008 |
Under one cent per week. Compare that to the 10+ hours of manual work these scripts replace.
What These Scripts Do Not Do
These scripts generate structured drafts. They do not:
- Replace your judgment on pricing or scope decisions
- Send emails automatically (you review first)
- Handle edge cases like multi-currency invoicing
- Integrate with accounting software (but the structured output makes integration straightforward)
The scripts give you a 90% complete document in seconds. You spend 5 minutes reviewing and customizing instead of 45 minutes writing from scratch.
The full source code for all 5 scripts is under 300 lines total. Clone it, swap in your own Pydantic models, and you have a custom automation toolkit for your specific freelance workflow.
Follow @klement_gunndu for more AI productivity content. We're building in public.
Top comments (0)