# Purchase approvals with spend limits and an audit trail

Canonical: https://useformwork.com/use-cases/purchase-approvals

Updated: 2026-10-09

A purchase approval form with line items and a calculated total that routes each request by spend limit to budget holders, finance and directors.

A purchase approval form has one job: get the right people to agree to a spend before anyone orders, and leave a record of who agreed. This walkthrough sets one up in FormWork for a fictional manufacturer, with line items, a calculated total, limits by amount and a hand-off to purchasing. It is a configuration pattern, not a case study or finance advice.

## The job: from request to purchase order

Without a system, the request is an email with a quote attached. The budget holder replies "fine", someone forwards it to finance, and months later nobody can say who approved it. Done means the request is complete, everyone your policy requires has approved it, and purchasing has raised the order.

## Build the purchase request form

Use three pages:

1. **Request**: name, **Work email**, **Cost centre** and the reason. Cost centre is a **Dropdown** whose options come from a **Cost centres** [data table](/docs/data-tables/overview) with the name, code and budget holder's email. A read-only **Email** field, Budget holder, uses **Look up from a dropdown** to copy that address from the chosen row.
2. **Items**: **Supplier**, a dropdown from an Approved suppliers table, with **Other** for new suppliers. Then a [repeating group](/product/nested-data), Items, with **Min items** set to 1: Description, Quantity, Unit price (a **Number** with a £ prefix) and Line total. A **Supplier quote** upload shows, and is required, when the total is at least 500.
3. **Check and send**: a paragraph that repeats the total.

Say in help text whether prices include VAT, and set your limits on the same basis. New suppliers need their own checks; see [supplier onboarding](/use-cases/supplier-onboarding).

## Calculate line totals and the order total

Each line's **Line total** is a **Calculation**: quantity times unit price, rounded to 2 places, using answers from the **Current instance** of the group. Turn on **Read only**.

The **Total** field sits below the group. Its calculation inserts Line total with **+ Reference**, adds the **Extract field** and **Join with separator** formatters, and wraps the result in `SUM`, which written out is `=SUM({{entry:current.answers.items|pluck:"line_total"|join:","}})`. Turn on **Read only** here too. The server works values out each time an answer is saved; see [calculated values](/docs/logic/calculations). Make Quantity and Unit price required so every line has a total, and test the result with the cases below.

## Set purchase approval limits by amount

This example's policy: the budget holder approves everything, finance also approves from £500, and two directors also approve from £5,000. Set up the [process](/product/process) on the form's **Process** page:

| Stage | Kind | Owner and due time | Moves on with |
| --- | --- | --- | --- |
| Budget holder approval | Waiting, needs approval | No owner, due in 3 days | Approved: Checking spend limit. Rejected: Rejected. **Send to finance**, to Finance approval, usable by the Finance group |
| Checking spend limit | Open | The Finance group, due in 1 hour | The routing automation below |
| Finance approval | Waiting, needs approval | No owner, due in 3 days | Approved: Checking spend limit. Rejected: Rejected |
| Director approval | Waiting, needs approval | No owner, due in 5 days | Approved: Ready to order. Rejected: Rejected |
| Ready to order | Open | The Purchasing group, due in 1 day | **Mark as ordered**, or **Cancel** with a required comment |
| Ordered, Rejected, Cancelled | Closed | None | |

Entries start in Budget holder approval when they are submitted. An approval stage always sends an approved entry to one fixed stage, so the amount is checked in one place. An automation, **Route by spend limit**, uses the **Stage changes** trigger with **To** set to Checking spend limit, and an **If** step:

- Previous stage is Budget holder approval and Total **Is at least** 500: **Move to stage** Finance approval.
- Previous stage is Finance approval and Total **Is at least** 5,000: **Move to stage** Director approval.
- **Otherwise**: **Move to stage** Ready to order.

Checking spend limit has a short due time so that, if the automation fails, the entry shows as overdue for Finance. For a single limit, the **Approve requests over an amount** automation template is a simpler start. See [automations](/product/automations) and [steps](/docs/automations/steps).

## Who approves: budget holders, finance and directors

Each approval stage has its own approvers and **Approvals needed** rule:

- **Budget holder approval**: **An email answer on the form**, the Budget holder field, with **Any one**. Budget holders approve from the email, without a FormWork account.
- **Finance approval**: **A group**, Finance, with **Any one**.
- **Director approval**: **A group**, Directors, with **At least** 2, and **Ask for a comment when approving** turned on.

Set **Links expire after** to 7 days. Under **Choose what approvers can see**, tick the cost centre, supplier, Items, Total and reason; the choice applies to every approval stage. Files show by name only on the approval page, so put what approvers need in answers. See [approvals](/docs/process/approvals).

To chase, add an automation with **In a stage too long** on the approval stages, for longer than 2 days, and a **Remind approvers** step. Don't escalate Director approval to another group with **Request approval**: that group's decision moves the entry on whatever the stage asked for, undoing the two-director rule.

## Hand off to purchasing and accounts

Purchasing sees requests that reach Ready to order in [My work](/docs/process/my-work) and takes one with **Assign to me**. Keep their details in [team fields](/docs/entries/team-fields): PO number, Purchasing notes and a **Yes or no** for Goods received.

An automation with **Stage changes** to Ready to order uses **Send to webhook** to create a draft purchase order in your accounting system, sending the supplier, cost centre code, lines and total, with the API key in a header as **A secret**. Set **Retries** to **Retry twice**, and under **If it fails** email the purchasing inbox. **Update entry or row** then saves the returned number in PO number. An [API connector](/docs/extensions/api-connectors) works too.

This doesn't prove the integration works: use **Test this request**, check [automation runs](/docs/automations/runs), and turn on **Email admins when an automation run fails** in [notifications](/docs/forms/notifications).

## Keep requesters informed

Turn on **Email the person who filled it in**, sent to Work email. With [respondent status](/docs/process/respondent-status) on, the confirmation links to a read-only page showing neutral labels: "Waiting for approval" for the approval stages and Checking spend limit, then "Approved, being ordered", "Ordered", "Not approved" and "Cancelled". An automation with **Stage changes** to Rejected can email the requester the approver's **Comment**.

## Controls and limits to decide on

- **Self-approval.** With that confirmation on, an approver whose address matches Work email is skipped. If nobody is left, the request waits until Finance uses **Send to finance**.
- **Requester identity.** Requesters don't sign in; Work email is what they type. Add a validation rule that it ends with your domain.
- **Access.** Everyone in the account can open every request. Budget holders who only approve can stay outside it. See [team members and roles](/docs/platform/users-permissions).
- **Forwarded emails** forward a working link, though links work once and expire.
- **Sequence.** Approvals run one stage at a time, and due times count calendar time.

## Test these cases before you publish

- Totals of £499.99, £500.00, £4,999.99 and £5,000.00.
- One line, ten lines, and a line removed after the total was shown.
- A budget holder raising a request on their own cost centre.
- A cost centre with no budget holder email.
- A Directors group with only one member: **At least** 2 drops to the number asked.
- A rejection with its comment, an expired link and **Remind now**.
- A webhook that fails or times out.

## Change limits safely

Change limits in the routing If and publish a [new form version](/product/versioned-forms). Requests keep the questions they were answered with, but the process and automations follow the latest version, so new limits apply the next time a request reaches Checking spend limit. The If compares stage names: rename a stage and update the If in the same publish. See [drafts and publishing](/docs/forms/form-versions).

## What success looks like

Every order traces back to a request, the approvals your policy requires and a PO number from a handoff that succeeded. Each entry's [history](/docs/entries/revisions) shows every request, decision and move, and an [export](/docs/entries/import-export) gives finance a list for audit. Set your own targets for approval times; this page doesn't claim any.

For a similar pattern with a cost estimate, see [travel requests](/use-cases/travel-requests). Compare [plans](/pricing) or [talk through your approval policy](/#contact).