Vercel Logo

Who handles “please cancel”?

Alex wants to stop next month's renewal and keep the rest of the month they've paid for. Should billing handle that, or should technical support change an account setting? We need to tell Jev which team is responsible.

In the repository you created on Vercel, open lib/examples.ts and find examples.contact.destinations, the list of teams the contact form can choose. The criteria for Billing / Invoices currently say:

lib/examples.ts: original billing_invoices criterion
criteria:
  "Invoice explanations, billing details, tax information, charges, or corrections, without a request to return money.",

“Corrections” leaves plenty of room for interpretation. Before we change the wording, let's decide which requests billing should receive.

Outcome

Write an explicit cancellation policy and test the same request before and after applying it.

Hands-on exercise

Open your copy locally

To edit the app, we'll clone our GitHub repository: download a local copy of its code. Install Git, Node.js 22 or later, and pnpm 10 if they're missing. Node runs JavaScript on your computer; pnpm installs the project's dependencies and runs its commands.

In your GitHub repository, open Code and copy the HTTPS URL. Open a terminal, such as Terminal on macOS or PowerShell on Windows, in the folder where you keep your projects. Run these commands, replacing YOUR_REPOSITORY_URL with the URL you copied:

git clone YOUR_REPOSITORY_URL jev-form-router
cd jev-form-router

The first command downloads the code into a folder called jev-form-router. The second moves the terminal into that folder. Open the same folder in your preferred code editor.

Run the following commands one at a time in that terminal, from the project folder containing package.json:

pnpm install --frozen-lockfile
pnpm dlx vercel link
pnpm dlx vercel env pull .env.local

pnpm install installs the dependencies using the versions in the lockfile. pnpm dlx vercel runs Vercel's command-line tool. Sign in when prompted, then link to the same team and existing project you deployed in Meet Jev.

The final command writes local settings to .env.local, including an OIDC token stored as VERCEL_OIDC_TOKEN. This identifies the project so the app can authenticate with AI Gateway while running on your computer.

Start the local app:

pnpm dev

Leave this command running; it serves the app while we work. Open http://localhost:3000/contact, using the terminal's port if it differs. Use this local page for the remaining exercises. Saving a file updates the local app; the deployed site still has the original code.

Move routing-notes.md into the project folder. When we run checks later, open a second terminal tab or window in this same folder so the app can keep running in the first one.

Try the cancellation request

Before editing the rules, load Overlapping needs on your local /contact page to fill in Alex's name and email. Change Subject to Cancel next renewal and Account context to Pro plan, workspace Daybreak. Replace Your message with:

Please cancel next month's renewal. Keep my access until the current paid month ends. I am not asking for money back.

We want Billing / Invoices to handle this request. Click Route submission and save the returned team and Selected by in routing-notes.md. This is our baseline, the result before changing the rules. We'll repeat the same request after the edit.

Decide which team should handle each request

Pick a team for each request before reading the explanation below. Our choices are Billing / Invoices, Billing / Refunds, Support / Account access, and Contact / Triage. Triage handles requests that need clarification before we can assign them to a specialist.

RequestWhat needs to happen first?
“Stop next month's renewal. This month's payment is fine.”Prevent a future charge.
“Cancel my renewal and return yesterday's payment. Please handle the refund first.”Return a collected payment.
“I want to cancel, but first help me sign in. I lost my authentication device.”Recover account access.
“Please cancel it.”Establish what the customer means.

Our team assigns future renewal changes to Billing / Invoices. Billing / Refunds handles requests to return money. An explicit request to recover access first goes to Support / Account access. When we cannot tell what the customer wants to cancel, Contact / Triage handles clarification.

Those are business rules. The model needs them because the phrase “please cancel” doesn't contain our team's org chart.

Put the policy in the right fields

Make these edits inside examples.contact:

  1. Extend billing_invoices.criteria to include cancellation of future renewals and turning off auto-renew while keeping paid access. Retain the restriction on returning money.
  2. Update support_technical.criteria so “turn off auto-renew” goes to billing. Otherwise its existing configuration responsibility overlaps with our new rule.
  3. Update instructions to resolve mixed requests using the immediate blocker or explicitly requested resolution. Preserve the access-first rule and send unclear cancellations to triage.
  4. Append Cancel next renewal to samples, using the form values above. The existing sampleIdentity object supplies Alex's name and email.

Write the criteria, then use them to assign the four requests again. Check that each request has a clear team. The Solution section below includes the complete replacements if you want to compare your edits.

Keep the existing destination IDs. The app uses those IDs to look up a team, label, and recipient. Changing a criterion expands an existing team's responsibility without adding another destination.

Keep the answer out of the evidence

Store the expected team in routing-notes.md. The sample should contain only the customer's form fields, so Jev has to choose a team from the request and our criteria.

Find the mapping from example.destinations to criteria in lib/router.ts. Both Jev and the fallback receive that question. Editing the destination definitions updates the policy supplied to either model.

Try It

Save lib/examples.ts, load Cancel next renewal on your local /contact page, and compare every field with your baseline. Submit it and record the result alongside the original, including Selected by.

If both runs choose invoices, we've removed an ambiguity without changing this particular answer. If the team changes, read the revised criterion beside the request and explain whether the change follows our rule. A different deciding model can also account for a different result.

In your second terminal, run the app's local checks:

pnpm validate

validate runs the checks defined in package.json: formatting and lint rules, TypeScript, unused code, and tests. The form-validation test checks every sample, including our new one.

The tests use mocks, replacements for the models that return fixed answers without making network calls. They can catch a malformed sample. Submitting the form checks how the models apply our new wording.

If something goes wrong

Routing worked on Vercel but fails locally: the local authentication token expires after 12 hours. In the terminal running the app, press Ctrl+C to stop it. Run pnpm dlx vercel env pull .env.local, then pnpm dev to restart it with a fresh token.

The new sample doesn't appear: save lib/examples.ts and check that the browser URL starts with localhost. The Vercel deployment still runs the original version.

Validation reports formatting errors: run pnpm fix in the second terminal, then pnpm validate again. The formatter also checks Markdown files such as routing-notes.md.

The sample appears on another page: the file also defines issue and lead forms. Put the entry in examples.contact.samples, alongside Limited context.

Refunds now go to invoices: check whether your billing criterion still excludes returning money. “All cancellation requests” is too broad for the routing rule we chose.

Checkpoint

Save lib/examples.ts and the before-and-after observations. Add a sentence explaining why cancellation and refund belong to different destinations.

Done-When

  • The criteria assign future renewals to billing without absorbing refunds.
  • The instructions resolve access-first and unclear requests.
  • The new sample reproduces the baseline's form values.
  • Validation passes and your notes contain the revised live result.

Solution

Replace the existing billing_invoices object with this complete entry:

lib/examples.ts: billing_invoices
{
  criteria:
    "Invoice explanations, billing details, tax information, charges, corrections, or cancellation of future renewals without a request to return money. This includes turning off auto-renew while keeping access for the current paid period.",
  id: "billing_invoices",
  specialty: "Invoices",
  team: "Billing",
},

The ID stays the same, so the app can still look up the team. Replace support_technical with:

lib/examples.ts: support_technical
{
  criteria:
    "A customer needs help with a malfunction, integration error, configuration, or product usage; account recovery is not the primary issue. Requests to cancel a future renewal or turn off auto-renew belong to billing_invoices.",
  id: "support_technical",
  specialty: "Technical help",
  team: "Support",
},

The contact-level instructions resolve mixed requests. Replace only examples.contact.instructions:

lib/examples.ts: contact instructions
instructions:
  "Choose the team that can resolve the main request. Distinguish a request to return money from a request to explain or correct an invoice. Route cancellation of future renewals without a refund request to billing_invoices. A request to return an already collected payment belongs to billing_refunds. For mixed topics, select the owner of the immediate blocker or explicitly requested resolution. If the customer explicitly asks to recover account access first, select support_access. A billing mention alone does not make a message a billing request. Use contact_triage if no primary need can be established, including cancellation requests that do not identify what should be canceled.",

Finally, append this entry to examples.contact.samples:

lib/examples.ts: cancellation sample
{
  label: "Cancel next renewal",
  values: {
    ...sampleIdentity,
    accountContext: "Pro plan, workspace Daybreak",
    message:
      "Please cancel next month's renewal. Keep my access until the current paid month ends. I am not asking for money back.",
    subject: "Cancel next renewal",
  },
},

Both models receive the updated criteria because lib/router.ts builds their shared routing question from these destination definitions.

Was this helpful?

supported.