Home / Lawn Care / Spring renewal: one Keep / Change / Skip link per customer, nothing renews by default
Lawn Care problem 4: Every spring the owner calls last year's customers one by one to find out who's coming back
Spring renewal
One Keep / Change / Skip link per customer, nothing renews by default
You fill a renewal sheet with last year's customers, their service and this year's price. From your start date each customer gets one message with a signed personal link to a one-page form: Keep, Change or Skip, plus a notes box. Keep creates a Jobber request with the service and price and tells the office; Change and Skip go to the office. One reminder goes out 7 days later; the day after your deadline the office gets one call list of everyone still silent. A row without a new price is never sent. Nothing renews unless the customer presses Keep.
How the workflow runs
Trigger · When an event arrives
Customer answers (POST)
Steps
- Verify + record answer once
- Keep -> Jobber request; others -> office
- Jobber items
- Office-only items
- Jobber: requestCreate
- Office email (with Jobber result)
Outcome
- Show result
- Send office emails
Also runs on: Every day 10 am → Daily renewal desk (only node that writes send state) → Send links + reminders
Also runs on: Customer opens link (GET) → Render renewal page → Show page
Drawn from the node names in the workflow file inside the download (workflow.json).
The problem
One commercial-contractor survey and an owner forum thread; retention matters, but we found no residential renewal-rate figure, so we cite none.
- In Aspire's 2025 survey of commercial landscape contractors, repeat business was 35% of revenue and customer retention was a top-three risk (30%). Source: Aspire: 2025 Landscape Industry Report (March 18, 2025) (Industry survey)
- LawnSite owners debate whether to auto-renew last year's contracts. LawnSite blocks automated readers, so we link the thread and quote nothing from it. Source: LawnSite thread: Auto renew previous years contracts? (Forum thread)
Who it's for
US seasonal lawn care companies (mowing, fertilizer programs, cleanups) that restart customers every spring.
Tools and monthly cost
| Tool | Official price |
|---|---|
| Jobber (you probably already pay for it) | Core $29 / month billed annually ($39 with a 1-year monthly commitment, $49 with no commitment); Connect from $99; Grow from $149; Plus from $399 (5 users). Jobber's developer docs say custom API integrations are only for approved customers, and the pricing page doesn't say which plan an API app needs pricing page |
| Twilio US local phone number | $1.15 / month pricing page |
| Twilio US SMS, sent or received | $0.0083 per segment, plus carrier fees on outbound texts of $0.0035-$0.005 per segment (AT&T $0.0035, T-Mobile $0.0045, Verizon $0.005) pricing page |
| A2P 10DLC registration (required to text US customers from a local number) | One-time: $4.50 brand registration (Standard brands add $41.50 secondary vetting) and $15 campaign vetting. Monthly: $1.50-$10 per campaign by use case ($2 for sole proprietors) pricing page |
| Resend (sends the emails; any email API that accepts an Idempotency-Key works) | Free: 3,000 emails / month, 100 / day. Pro: $20 / month for 50,000 emails pricing page |
| Google Sheets (opt-out list, replies, renewal list) | Free with a Google Account; Google Workspace is optional pricing page |
| n8n | Community Edition: free if you self-host (you pay for the server). n8n Cloud: Starter EUR 20 / month or Pro EUR 50 / month, billed annually pricing page |
Example month: Illustrative arithmetic, not a measured result. One local number sending 150 single-segment texts and receiving 150 a month, at the highest listed carrier fee ($0.005): 150 x ($0.0083 + $0.005) + 150 x $0.0083 + $1.15 number = $4.39 a month in Twilio usage, plus the A2P 10DLC campaign fee ($1.50-$10 a month) and one-time registration ($4.50 brand + $15 campaign vetting). Emails stay inside Resend's free tier at this volume. n8n Cloud Starter (EUR 20 a month) or your own server, and your Jobber plan, come on top. Your volumes will differ.
Setup
- Jobber: create a Jobber developer app, add an n8n OAuth2 credential (authorization URL https://api.getjobber.com/api/oauth/authorize, token URL https://api.getjobber.com/api/oauth/token). n8n refreshes the token and stores the rotated refresh token. Set MSS_JOBBER_URL=https://api.getjobber.com/api/graphql. The X-JOBBER-GRAPHQL-VERSION header is pinned in the workflow.
- Texts need a Twilio number on an approved A2P 10DLC campaign. Set TWILIO_ACCOUNT_SID, TWILIO_BASIC_AUTH (base64 of SID:auth token) and LAWN_TWILIO_FROM. Texts only go out 8 a.m.-9 p.m. in the customer's time zone and only with recorded consent.
- Email: MSS_EMAIL_API (e.g. https://api.resend.com), EMAIL_API_KEY, LAWN_EMAIL_FROM, LAWN_OFFICE_EMAIL. Every send carries an Idempotency-Key, so a retried run never emails twice.
- Company details: LAWN_COMPANY_NAME, LAWN_COMPANY_PHONE, COMPANY_TZ. Every message names your company.
- Google Sheet stand-in: MSS_SHEET_URL points at the shared sheets (sms_optouts, inbound_texts and the recipe's own sheets). In production, swap these HTTP calls for n8n's Google Sheets node.
- Remove the test-only mss-lc*-tick webhook (or put it behind n8n header auth) and make sure MSS_TEST_CLOCK is NOT set in production.
- Sheet renewal_list columns: id, first_name, last_name, phone, email, sms_ok (yes only if they agreed to texts), jobber_client_id, service, frequency, last_price, new_price.
- LAWN_RENEWAL_START and LAWN_RENEWAL_DEADLINE (YYYY-MM-DD) and LAWN_LINK_SECRET (a long random string; links are HMAC-signed and a changed link shows 'This link is not valid').
- Keep creates a request on the client's Jobber record; if Jobber fails, the office gets 'create it by hand' with every detail.
Download
Downloadlawn-spring-renewal.zip
Contains the n8n workflow, the mock data, the test runner with its last results, and the mock server we tested against. All keys in .env.example are fake.
Test result
Every workflow was imported into n8n 2.45.4 on an isolated test instance and run end to end. Jobber (GraphQL, OAuth2 with refresh-token rotation and signed webhooks), Twilio, the National Weather Service API, the email API (Resend-style) and Google Sheets were replaced by a local mock server built from the vendors' public docs: same request shapes and auth headers, HMAC-signed webhooks, HTTP 429 with Retry-After and Jobber THROTTLED errors. A frozen test clock checks the 8 a.m.-9 p.m. texting window. These recipes use no AI model; every decision is a written rule. Customers, yards, invoices and addresses are invented. Verified with mocks; nothing was sent to a real person or account. How we test.
17/17 scenarios passed (run 2026-10-31)
| PASS | L4-01 | Before RENEWAL_START: nothing is sent |
| PASS | L4-02 | Start day: each customer gets a link with this year's price (text if they agreed to texts, else email); a row with no price goes to the office instead |
| PASS | L4-03 | Next day: no repeats (including the no-price office alert) |
| PASS | L4-04 | Customer opens the link: page shows the service, last year's and this year's price, Keep / Change / Skip, and says nothing renews without Keep |
| PASS | L4-05 | Sheet text is HTML-escaped on the page (no injected markup) |
| PASS | L4-06 | Forged link (other customer's id, or tampered signature): 403, nothing shown |
| PASS | L4-07 | Dana presses Keep: thank-you page, answer logged to the sheet, Jobber request created with this year's price, office emailed with her notes |
| PASS | L4-08 | Second submit (double tap or changed mind): not recorded twice, no second Jobber request |
| PASS | L4-09 | Reopening the link later shows the saved answer |
| PASS | L4-10 | Raj presses Change: office gets it with his note (call within 2 business days); no Jobber request |
| PASS | L4-11 | Honeypot filled (bot): polite page, nothing recorded |
| PASS | L4-12 | 7 days after the link: one reminder to customers who haven't answered (Lena), none to those who did |
| PASS | L4-12b | Later: no second reminder |
| PASS | L4-13 | Day after the deadline: the office gets one call list of non-responders (Lena, Tom), no more customer messages |
| PASS | L4-13b | Next day: call list not repeated |
| PASS | L4-14 | Jobber THROTTLED on Keep: customer still sees the thank-you page; office told to create the request by hand |
| PASS | L4-15 | Customer who texted STOP earlier gets the renewal by email instead |
Actual output from the test run
Scenario L4-02: the renewal text with the personal Keep / Change / Skip link (link signature shortened; invented customer). Names and numbers are invented.
H
Verified with mocks
- Before RENEWAL_START: nothing is sent
- Start day: each customer gets a link with this year's price (text if they agreed to texts, else email); a row with no price goes to the office instead
- Next day: no repeats (including the no-price office alert)
- Customer opens the link: page shows the service, last year's and this year's price, Keep / Change / Skip, and says nothing renews without Keep
- Sheet text is HTML-escaped on the page (no injected markup)
- Forged link (other customer's id, or tampered signature): 403, nothing shown
- Dana presses Keep: thank-you page, answer logged to the sheet, Jobber request created with this year's price, office emailed with her notes
- Second submit (double tap or changed mind): not recorded twice, no second Jobber request
- Reopening the link later shows the saved answer
- Raj presses Change: office gets it with his note (call within 2 business days); no Jobber request
- Honeypot filled (bot): polite page, nothing recorded
- 7 days after the link: one reminder to customers who haven't answered (Lena), none to those who did
- Later: no second reminder
- Day after the deadline: the office gets one call list of non-responders (Lena, Tom), no more customer messages
- Next day: call list not repeated
- Jobber THROTTLED on Keep: customer still sees the thank-you page; office told to create the request by hand
- Customer who texted STOP earlier gets the renewal by email instead
- Verified with mocks; invented customers, yards, invoices and addresses only.
Documented from official API docs only
Built to the vendor's documentation but not run against a live account. Check these on yours.
- Jobber GraphQL field and mutation names, webhook signing (X-Jobber-Hmac-SHA256) and OAuth refresh-token rotation (from Jobber's developer docs; not run against a live account).
- Twilio request signing, Messages API, 429 behavior and built-in STOP handling (from Twilio's docs). The email API follows Resend's documented request shape.
Limitations
- Opt-in on purpose: many states have automatic-renewal laws with notice and consent rules. We're not lawyers; check your state before adding any auto-renew wording.
- Anyone with a customer's link can answer for them; the office confirms Keep answers before scheduling.
- Every message is checked against a word list first: safety claims like 'pet safe' or 'non-toxic' (the kind 40 CFR 156.10 treats as misbranding on a pesticide label), 'guaranteed' results and collection threats are blocked, and the office is told instead.
- Written and tested for Jobber's API only. LMN, Aspire, Service Autopilot and other software are not built for this recipe, and we make no claim that it works with them. Jobber's developer docs say custom integrations are only possible for approved Jobber customers, so check with Jobber first.
- Not legal advice and not a compliance guarantee. Texts go only to customers with recorded consent, only between 8 a.m. and 9 p.m. in the customer's time zone, and plain-English opt-outs are honored (47 CFR 64.1200 is the federal rule we design around). Some states are stricter than federal law; have your attorney review texting consent before going live.