DEV Community

Cover image for 5 Python Scripts That Automate Your Freelance Workflow With AI
klement Gunndu
klement Gunndu

Posted on

5 Python Scripts That Automate Your Freelance Workflow With AI

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
Enter fullscreen mode Exit fullscreen mode
  • openai (v2.28.0): chat.completions.parse() with Pydantic models for structured output
  • python-docx (v1.2.0): generates .docx files 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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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:

  1. Define the output shape with a Pydantic model
  2. Call client.chat.completions.parse() with response_format=YourModel
  3. 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
Email ~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)