name: upwork-proposal description: Write a short, targeted Upwork proposal from a job description and the user's resume, humanized for natural tone. argument-hint: "<proposal_id | job_description>"
Upwork Proposal Generator
Write a concise, winning Upwork proposal that directly addresses the client's needs. Clients skim proposals on mobile, between meetings — if it takes more than ~30 seconds to read, it's gone. Optimize every line for a fast skim.
Setup
Follow ../shared/setup.md. Then Read the resume at data.primaryResumeSourceAbsolutePath for full context (identity, skills, experience, projects, research).
Step 1: Resolve the Input
The argument is either a proposal id (an integer, when launched from the JobPilot UI) or a raw job description (manual use). Detect which:
Integer id → fetch the draft row and use its stored job details as the JD:
curl -fsS "$JOBPILOT_API/api/upwork/proposals/$ARG"Use
data.jobDescriptionas the posting, plusdata.jobTitle/data.clientName/data.jobUrlfor context. Remember the id — you willPATCHthe result back to it in Step 7. (A draft launched from an Upwork search recommendation already has these fields filled andsource:"search"— same flow, no extra work.)Anything else → treat the argument itself as the job description. There is no row yet; you will
POSTa new one in Step 7.
Step 2: Analyze the JD
Identify: what the client needs built/fixed, required tech and skills, scope and timeline clues, pain points/challenges, and any specific questions the client asks. Pull out one concrete detail unique to this posting — you'll reference it in the hook so the client can tell the proposal isn't a mass send.
Step 3: Select ONE Matching Case Study
Pick the SINGLE most relevant project from the resume — one that matches their problem, not just their tech stack. One specific, on-point case study beats five generic ones. Don't list everything. If a portfolio/GitHub/live link exists for it, keep that link ready.
Step 4: Write
Order it the way it gets read — them first, you second, never the reverse.
Hook (line 1, bolded): Open with a bold line that names their problem or goal in their words. Upwork renders **text** as bold — use it on this line so it stands out instantly. This is the single most important line; if it reads like "Hello, I'm excited to apply," the proposal is deleted. No "Hi" / "Dear client" / "I'm excited to apply" / "I came across your posting."
Case study (1–2 lines): One relevant project, stated specifically — what you built, the outcome with a real metric (users, perf gain, revenue), and a link if available. Tie it directly to what they need. Don't narrate your career; the client cares about their problem, not your journey.
Specific question (1 line): Ask ONE sharp question about their project that proves you read the posting and thought about it (scope, an edge case, a decision they'll need to make). This both shows engagement and opens a conversation.
CTA (1 line): End with a real next step — the specific question above, or a calendar link if the resume/profile provides one. Never end with a dead phrase like "Looking forward to hearing from you." Create the next step.
Step 5: Answer Screening Questions
If the posting has screening questions, answer each one short, direct, and specific. "How many years with React?" → "4 years, including [project]." Never write a 300-word essay for a one-line question — long answers read as AI padding.
Step 6: Apply Humanizer
Invoke the humanizer skill on the full text.
Step 7: Persist to JobPilot
Save the result so it appears on the Upwork page. screeningAnswers is a JSON array of { "question", "answer" } objects (empty [] if the posting had none).
Launched with an id →
PATCHthe existing draft (status staysdraft):curl -fsS -X PATCH "$JOBPILOT_API/api/upwork/proposals/$ARG" \ -H 'content-type: application/json' \ -d '{ "proposalText": "...", "screeningAnswers": [] }'Launched with a raw job description →
POSTa new row:curl -fsS -X POST "$JOBPILOT_API/api/upwork/proposals" \ -H 'content-type: application/json' \ -d '{ "jobTitle": "...", "clientName": "...", "jobUrl": "...", "jobDescription": "...", "proposalText": "...", "screeningAnswers": [] }'jobTitleis required; derive it from the posting. IncludeclientName/jobUrlwhen the posting provides them.
Then print the proposal (and any screening answers, each labeled with its question) to the terminal so the user can paste it into Upwork.
Rules
- Under 150 words for the body. Shorter wins on mobile. Brevity reads as confidence.
- Them before you. Their problem leads; your background supports. Never open with 3 paragraphs about yourself.
- No AI tells. No "Certainly, here is..." preambles, no robotic symmetry, no functional emoji bullets (✅ 📌 🔹 ☑) — those scream AI. If an emoji appears at all, at most one, used the way a person would.
- No fluff — drop "passionate", "dedicated", "committed", "excited", "thrilled", "leverage", "utilize", "innovative", "cutting-edge", "seamless", "robust".
- No generic openings ("I came across your job posting" / "I'm a senior developer with X years").
- Be specific. Real project names, metrics, tech, and links from the resume — referencing their actual job, not a template.
- One matched case study, not five. Relevance beats volume.
- One CTA, and make it a real next step (a question or a calendar link).
- Match tone. Casual posting → casual; formal → professional.
- No fabrication. Only reference resume content. Don't invent metrics or links.
- Don't mention freelance status (Top Rated, JSS) in body — it's already on the profile. Exception: if the posting explicitly asks.
- First person as the candidate.
Output
Plain text proposal that pastes into Upwork's input. Use **bold** only on the line-1 hook (and sparingly on one key phrase if it genuinely helps a skim) — no markdown headers, no bullet lists in the body. Output screening-question answers separately, each labeled with its question.