Dock
Sign in & remix
REMIX PREVIEWTemplate

Field service job summary and invoice prep

Reads technician job completion notes, extracts labor and parts with Claude, calculates a full invoice (labor, parts, tax), and routes it to a reviewer before emailing the customer.

· 14 min read· from trydock.ai

Agent: read this first

You're working in the Field service job summary and invoice prep workspace at trydock.ai/<org>/field-service-job-invoice.

## Your role

Watches jobs-inbox/ on cron, extracts job fields with Claude, computes invoice totals against LABOR_RATE + TAX_RATE, creates Jobs queue rows at Status=Pending Review, posts Slack review requests to REVIEWER_SLACK_ID. Never flips Status to Approved or emails the customer.

## Cadence, when to update Dock

- After EVERY tool call that changes state (row create / update, doc append), append an entry to the **Status** surface.
- After every milestone, update the canonical doc surface for that work.
- When you start a session, READ the last 5 Status entries first to know what happened since you last worked.

## User-loop protocol

- End EVERY reply to the user with: "Check Dock for the latest at trydock.ai/<org>/field-service-job-invoice."
- If you make a decision the user might want to override (priority, scope cut, tone change), append to **Status** AND mention it in your reply: "Decided X. If you'd rather Y, edit [surface] and I'll re-plan."
- If you're blocked, post to Status with prefix `BLOCKED:` and ask one specific question.

## Don't touch

- Canonical phase / column / surface titles.
- Anything in a section titled "Locked" or "Decisions sealed."
- The agentPrompt itself (this section).

## First tool calls

1. `list_surfaces(workspace_slug="field-service-job-invoice")`
2. `get_doc(workspace_slug="field-service-job-invoice", surface_slug="status")`
3. `list_rows(workspace_slug="field-service-job-invoice", surface_slug="jobs-queue")`

---

# Field service job summary and invoice prep

A two-step pipeline that turns technician job notes into reviewed customer invoices. Open in Dock and you get four surfaces seeded:

- **Jobs queue** (table) one row per job: Job Number, Customer, Customer Email, Address, Technician, Service Type, Date Completed, Labor Hours, Parts Cost, Total Amount, Status, Job File, Notes, Submitted At, Sent At.
- **Setup guide** (doc) how the agent watches the inbox folder + the two Python scripts + labor rate + tax rate + reviewer configuration.
- **Invoice log** (doc) running narrative log of every invoice generated, useful for month-end review and revenue tracking.
- **Status** (doc) the agent's working session log. End of every run: 1 paragraph of what was processed, what's pending review, what was sent.

## Bring your own agent

Connect one agent to this workspace (Claude in Cursor, Claude Code, Codex, any MCP client). The agent watches the inbox folder on cron, no manual trigger needed once schedules are wired.

## The user-loop protocol

Your agent proposes; you decide.

- Hourly: agent scans jobs-inbox/ for new .txt or .md files. Skips names in processed_jobs.json. For each new file, extracts job_number, customer, customer_email, address, technician, service_type, date_completed, labor_hours, parts (array of description + quantity + unit_price), and a 1-2 sentence notes summary via Claude.
- Calculates labor_cost = labor_hours * LABOR_RATE, parts_cost = sum(qty * unit_price), subtotal, tax = subtotal * TAX_RATE, total. Creates Jobs queue row at Status=Pending Review. Posts Slack review request to REVIEWER_SLACK_ID with line items.
- Reviewer opens Dock, walks the math, flips Status to Approved (or Rejected). Twice daily 9 AM + 5 PM: send_invoice.py sweeps Approved rows without a Sent At timestamp, emails the customer via SMTP, marks Sent.
- Agent NEVER flips Status to Approved. Agent NEVER emails the customer. Those are operator decisions.
- End of every working session, agent writes 1 paragraph to **Status**: jobs processed, pending review, sent.

## First run

1. Confirm rates with the operator: LABOR_RATE (hourly), TAX_RATE (decimal, 0 if not taxing labor), REVIEWER_SLACK_ID, FROM_NAME for outgoing emails.
2. Get SMTP credentials: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD. Gmail requires App Passwords.
3. Open Setup guide and follow the install steps. Configure .env, drop scripts.
4. Drop 1-2 test job files (plain text) into jobs-inbox/. Run generate_invoice.py once manually. Watch Jobs queue + Slack + Status.

## Status

### [00:00 UTC] seeded by Dock
Workspace created from the field service job summary and invoice prep template. Agent: read the Setup guide, then confirm rates + SMTP credentials + reviewer Slack ID with the operator before the first run.
Next: confirm rates + SMTP + reviewer, run first job test.

Outcome

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.

Estimated time: 60 min setup, ongoing ~2 min per job review
Difficulty: intermediate
For: Office managers and ops leads at HVAC, plumbing, electrical, pest control, landscaping, or facilities businesses.

What you'll need

Pre-register or install before you start.

  • Dock (Free plan covers this template; Pro $19/mo unlocks more workspaces.) — The workspace itself, the Jobs queue table, the Invoice log doc surface for month-end review.
  • Anthropic API (Pay per token, ~$0.005 per job extraction on Sonnet.) — Claude extracts customer, service type, labor hours, parts (with unit prices), and notes from each job sheet.
  • Slack incoming webhook (Free) — Review request destination. Block Kit message with customer + total + service type + line-item breakdown + reviewer mention.
  • SMTP (Free with Gmail; SendGrid + Mailgun have free tiers below 100/day.) — Customer email delivery. Gmail App Passwords work, or any SMTP provider (SendGrid, Mailgun, your hosting provider).
  • CueAPI (optional) (Free tier covers hourly + twice-daily cadence.) — Cloud-scheduled hourly invoice generation + twice-daily send so the pipeline keeps running when your laptop is closed.

The template · 5 steps

Step 1: Set rates + reviewer + outgoing email identity

Estimated time: 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')

[!CAUTION] 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.

Step 2: Wire .env + SMTP credentials

Estimated time: 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
  • Run pip install anthropic requests python-dotenv
  • Create .env with DOCK_API_KEY, DOCK_WORKSPACE_SLUG, ANTHROPIC_API_KEY, SLACK_WEBHOOK_URL, SMTP_HOST, SMTP_PORT=587, SMTP_USER, SMTP_PASSWORD, FROM_NAME, JOBS_INBOX_DIR=./jobs-inbox, LABOR_RATE=95, TAX_RATE=0.0875, REVIEWER_SLACK_ID, CURRENCY_SYMBOL=$, CLAUDE_MODEL=claude-sonnet-4-6
  • 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.

[!CAUTION] 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.

Step 3: Test the full review-and-send loop

Estimated time: 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?

[!CAUTION] 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.

Step 4: Schedule hourly generate + twice-daily send

Estimated time: 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
  • Option B, CueAPI: cueapi create --schedule '0 * * * *' --name 'field-service-invoice-generator' --handler ./generate_invoice.py and cueapi create --schedule '0 9,17 * * *' --name 'field-service-invoice-sender' --handler ./send_invoice.py
  • 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.

[!CAUTION] 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).

Step 5: Train techs on the job note format

Estimated time: 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.

[!CAUTION] 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.

Hand the template to your agent

Paste the prompt below into your agent's permanent system prompt so the agent reads, writes, and maintains this workspace as you work through the steps.

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")

FAQ

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.

Remix this into Dock

Make this yours. Edit, extend, run agents on it.

Sign in (free, 20 workspaces) — Dock mints a copy of this in your own workspace. The original stays untouched.

No Dock account? Sign-in is signup. Magic-link in 30 seconds.