A field service workspace where every completed job turns into a reviewed, emailed invoice within hours of the tech writing the job note, with a full audit trail in Dock.
A field service workspace where every completed job turns into a reviewed, emailed invoice within hours of the tech writing the job note, with a full audit trail in Dock.
Time60 min setup, ongoing ~2 min per job reviewDifficultyintermediateForOffice managers and ops leads at HVAC, plumbing, electrical, pest control, landscaping, or facilities businesses.
How this works
Open it, hand it to your agent, walk the steps.
Paste this to your agent (Claude / Cursor / Codex)
You are the agent running on the "Field service job summary and invoice prep" template workspace, connected via MCP at your-org/field-service-job-invoice.
Your job: watch jobs-inbox/, extract job fields with Claude, compute labor + parts + tax + total, create a Jobs queue row at Status=Pending Review, post a Slack review request to REVIEWER_SLACK_ID. Never flip Status to Approved. Never email the customer.
User-loop protocol:
- You propose. The operator decides. Never flip Status past Pending Review. Never call SMTP.
- Hourly (or "generate invoices"): list .txt/.md files in JOBS_INBOX_DIR. Skip names in processed_jobs.json. For each new file, run Claude extract on the contents.
- Extracted fields: job_number, customer, customer_email, address, technician, service_type, date_completed (YYYY-MM-DD), labor_hours (number), parts (array of {description, quantity, unit_price}), notes (1-2 sentence summary). unit_price is PER SINGLE UNIT, divide if the source gives a total.
- Compute totals: labor_cost = labor_hours * LABOR_RATE; parts_cost = sum(qty * unit_price); subtotal = labor_cost + parts_cost; tax = subtotal * TAX_RATE; total = subtotal + tax. Round all to 2 decimals.
- Build line items string: Labor line, one line per part (qty x unit_price = subtotal), Tax line if TAX_RATE > 0, TOTAL line.
- Create a Jobs queue row with all fields + Status=Pending Review.
- Post Slack review request to REVIEWER_SLACK_ID with header (customer name), section fields (customer, total, service, tech, date), section with the line items in a code block, and a back-link to Dock.
- Add the filename to processed_jobs.json on success. On json.JSONDecodeError: mark processed permanently. On transient error (Dock, Slack, network): do NOT mark, retry next run.
- End of every working session, write 1 paragraph to Status: jobs processed, pending review, sent.
Don't touch:
- Jobs queue.Status (Pending Review / Approved / Rejected / Sent / Approved no email is the operator's flow).
- Jobs queue.Sent At (send_invoice.py stamps this).
- LABOR_RATE / TAX_RATE in .env (the operator owns these).
First MCP tool calls:
1. list_surfaces(workspace_slug="field-service-job-invoice")
2. list_rows(workspace_slug="field-service-job-invoice", surface_slug="jobs-queue")
3. get_doc(workspace_slug="field-service-job-invoice", surface_slug="status")
Top to bottom. Each step has tasks, pointers, gotchas.
01 / 05
Set rates + reviewer + outgoing email identity
10 min
Three numbers and three identities drive the whole pipeline. LABOR_RATE (hourly, e.g. 95), TAX_RATE (decimal, e.g. 0.0875 or 0 if you don't tax labor), REVIEWER_SLACK_ID (the office manager or ops lead who approves invoices), SMTP_USER (the From address customers see), FROM_NAME (display name on outgoing emails).
Tasks
Set LABOR_RATE to your default hourly. The Extending section in Setup guide shows per-service-type rates if you bill different rates for HVAC vs plumbing.
Set TAX_RATE as a decimal. 0.0875 = 8.75%. Set to 0 if your jurisdiction doesn't tax labor.
Get REVIEWER_SLACK_ID: in Slack, click the reviewer's profile, More, Copy Member ID (Uxxxxx format)
Pick an outgoing email address (e.g. invoices@yourcompany.com) and FROM_NAME (e.g. 'Acme HVAC')
Gotchas
If you bill different rates for different services (HVAC vs plumbing), use a single LABOR_RATE for now and add per-service rates via the RATES dict extension in Setup guide.
TAX_RATE = 0 means the tax line is omitted from line items, not shown as 0%. Cleaner output.
02 / 05
Wire .env + SMTP credentials
20 min
SMTP is the trickiest piece for new operators. Gmail requires App Passwords (not your account password). Most hosting providers ship working SMTP credentials. SendGrid and Mailgun have free tiers below 100 emails/day, which covers most field service businesses.
Tasks
Gmail: turn on 2FA, then visit myaccount.google.com/apppasswords, generate an App Password, paste into SMTP_PASSWORD
SendGrid: sign up, create an API key with Mail Send scope, set SMTP_HOST=smtp.sendgrid.net, SMTP_PORT=587, SMTP_USER=apikey, SMTP_PASSWORD=the API key
Hosting provider SMTP: pull credentials from your provider's docs (DreamHost, Bluehost, etc.)
Open Setup guide (doc) and copy generate_invoice.py + send_invoice.py into a local folder
Generate a Dock API key at trydock.ai/settings/api
mkdir jobs-inbox; drop a test job note (customer, address, labor hours, parts list)
Run python generate_invoice.py once manually. Confirm a Jobs queue row appears + a Slack review request lands.
Gotchas
Gmail with regular account password = 535 auth error. App Passwords are required.
If you skip the test send (Step 3) you'll discover SMTP issues live in front of a customer. Always send to yourself first.
Agent prompt for this step
Run a first job generation. List new files in JOBS_INBOX_DIR. For each, run Claude extract (job_number, customer, customer_email, address, technician, service_type, date_completed, labor_hours, parts array, notes). Compute labor_cost, parts_cost, subtotal, tax, total. Build the line items string. Create a Jobs queue row at Status=Pending Review. Post the Slack review request. Append filename to processed_jobs.json on success. Post a Status entry summarizing: jobs processed, pending review.
03 / 05
Test the full review-and-send loop
15 min
Before scheduling, confirm extraction, math, review, and SMTP send work end to end. Drop a real job note (or a realistic test note with customer email = your own address). Walk the loop once with a human.
Tasks
Drop a test job note in jobs-inbox/. Include customer name, customer email (use your own), address, technician, service type, labor hours, 2-3 parts with quantities and prices, brief notes.
Run python generate_invoice.py. Confirm Jobs queue row + Slack review request + line items math matches what you'd expect by hand.
Open Dock Jobs queue. Flip the row Status from Pending Review to Approved.
Run python send_invoice.py. Confirm: Status flips to Sent + Sent At gets stamped + email arrives at your inbox + Slack gets a 'Invoice sent' confirmation.
Read the email. Is the body clear? Total correct? FROM_NAME readable?
Gotchas
If unit_price is wrong (e.g. parts cost looks doubled), the source job note probably said '2x capacitors $90 total' and Claude misread. Edit the job note format with the tech, or adjust the prompt in extract_job() to bias toward unit_price = per-single-unit.
No customer email on the job note: send_invoice.py logs the skip + sets Status to 'Approved no email' so the reviewer knows to send manually.
04 / 05
Schedule hourly generate + twice-daily send
10 min
Two cron tasks: generate_invoice.py hourly to clear the inbox quickly, send_invoice.py at 9 AM and 5 PM so customers don't get emails at 2 AM. CueAPI is the right pick for cloud-scheduled runs.
Tasks
Option A, cron: crontab -e, add `0 * * * * cd /path && source .env && python3 generate_invoice.py >> generate.log 2>&1` and `0 9,17 * * * cd /path && source .env && python3 send_invoice.py >> send.log 2>&1`
Confirm next hour: Status has a fresh session entry, anything in the inbox flips to a Jobs queue row.
Confirm next 9 AM: an Approved row flips to Sent and the customer gets the email.
Gotchas
Twice-daily send is the right pace for field service. Hourly send means a customer might get the invoice at 11 PM, which reads as unprofessional.
If your team writes job notes throughout the day, hourly generate clears them fast. If techs batch at end of shift, switch generate to twice-daily (5 PM and 10 PM).
05 / 05
Train techs on the job note format
20 min one-time + 10 min/month ongoing
Extraction quality depends entirely on job note quality. The agent reads plain text, so the tech can write naturally, but a few habits make extraction more reliable. Give the techs a one-page cheatsheet.
Tasks
Write a cheatsheet: include customer name + address + service type at the top, labor hours as 'Labor: 2.5h' or 'Labor: 2h 30min', parts as 'Part name x qty $unit_price' or 'qty Part name @ $unit_price each'
Walk through 2-3 example job notes with each tech. Show the resulting invoice. Show what changes if they skip a field.
Month-end: walk Invoice log (doc) with the office manager. Spot extraction patterns that need fixing (e.g. one tech always writes prices as totals not per-unit) + retrain.
Gotchas
Techs who write 'used some new fittings $40' produce useless invoices. Be specific in the cheatsheet: 'always include quantity and per-unit price'.
If a tech uses a different format that the agent reads consistently, leave them alone. The cheatsheet is a default, not a rule.
FAQ
Common questions on this template.
What if the tech doesn't include the customer email on the job note?
send_invoice.py handles this gracefully. If Customer Email is blank when Status flips to Approved, the script logs the skip and sets Status to 'Approved no email'. The reviewer sees this state in Dock and sends the invoice manually (or chases the customer email).
Can I bill different labor rates for different service types?
Yes, via the RATES dict extension in Setup guide. Replace the single LABOR_RATE with a dict keyed by service_type (e.g. {'HVAC': 110, 'Plumbing': 120}) and look up the rate by the Claude-extracted service_type. The base template uses a single rate for simplicity.
Can I attach a PDF invoice instead of plain text email body?
Yes, with one extension: pip install reportlab or weasyprint, then in send_invoice.py generate a formatted PDF from the row data and attach to the email. The base template ships plain-text bodies because most field service customers don't actually need PDFs; the message and total are what matters.
What if my techs already use ServiceTitan or Jobber?
Replace the folder watcher in generate_invoice.py with an API fetch loop using the field service app's API. Use processed_jobs.json to dedupe on job IDs. The rest of the pipeline (extract, compute, Dock row, Slack, send) is unchanged. Setup guide notes this extension in the Extending section.
What happens if Claude misreads parts quantities or prices?
Reviewer catches it. The Slack review request shows full line items with the math; the reviewer walks the totals and flips to Approved or Rejected. If a vendor or parts list trips up extraction consistently, edit the prompt in extract_job() with a hint specific to that parts catalog.
Open this template as a workspace.
We mint a fresh copy in your org with the steps as table rows, the pointers as a separate table, and the brief as a doc. Bring your agents, start checking off boxes.